同时调用同一函数:死锁是如何发生的?


15

new_customerWeb应用程序每秒调用我的函数几次(但每个会话仅调用一次)。它要做的第一件事就是锁定customer表(执行“如果不存在,请插入”-的简单变体upsert)。

我对文档的理解是,对的其他调用new_customer应该简单地排队,直到所有先前的调用完成为止:

LOCK TABLE获取表级别的锁,必要时等待释放任何冲突的锁。

为什么有时会陷入僵局呢?

定义:

create function new_customer(secret bytea) returns integer language sql 
                security definer set search_path = postgres,pg_temp as $$
  lock customer in exclusive mode;
  --
  with w as ( insert into customer(customer_secret,customer_read_secret)
              select secret,decode(md5(encode(secret, 'hex')),'hex') 
              where not exists(select * from customer where customer_secret=secret)
              returning customer_id )
  insert into collection(customer_id) select customer_id from w;
  --
  select customer_id from customer where customer_secret=secret;
$$;

日志错误:

2015-07-28 08:02:58 BST详细信息:进程12380等待数据库12141的关系16438上的ExclusiveLock;被进程12379阻止。
        进程12379在数据库12141的关系16438上等待ExclusiveLock;被进程12380阻止。
        流程12380:选择new_customer(decode($ 1 :: text,'hex'))
        流程12379:选择new_customer(decode($ 1 :: text,'hex'))
2015-07-28 08:02:58 BST提示:请参阅服务器日志以获取查询详细信息。
2015-07-28 08:02:58 BST上下文:SQL函数“ new_customer”语句1
2015-07-28 08:02:58 BST语句:选择new_customer(decode($ 1 :: text,'hex'))

关系:

postgres=# select relname from pg_class where oid=16438;
┌──────────┐
 relname  
├──────────┤
 customer 
└──────────┘

编辑:

我设法得到了一个简单的可重现的测试用例。在我看来,由于某种竞争状况,这似乎是一个错误。

模式:

create table test( id serial primary key, val text );

create function f_test(v text) returns integer language sql security definer set search_path = postgres,pg_temp as $$
  lock test in exclusive mode;
  insert into test(val) select v where not exists(select * from test where val=v);
  select id from test where val=v;
$$;

bash脚本在两个bash会话中同时运行:

for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done

错误日志(通常是1000个调用中的少数死锁):

2015-07-28 16:46:19 BST ERROR:  deadlock detected
2015-07-28 16:46:19 BST DETAIL:  Process 9394 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9393.
        Process 9393 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9394.
        Process 9394: select f_test('blah')
        Process 9393: select f_test('blah')
2015-07-28 16:46:19 BST HINT:  See server log for query details.
2015-07-28 16:46:19 BST CONTEXT:  SQL function "f_test" statement 1
2015-07-28 16:46:19 BST STATEMENT:  select f_test('blah')

编辑2:

@ypercube 建议使用lock table外部函数的变体

for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done

有趣的是,这消除了僵局。


2
在同一笔交易中,在进入该功能之前,其customer使用方式会抢到较弱的锁?那么这可能是一个锁升级问题。
DanielVérité15年

2
我无法解释这一点。丹尼尔可能有一点。可能值得在pgsql-general上提出。无论哪种方式,您是否都知道即将发布的Postgres 9.5中的UPSERT实现?Depesz看看它。
Erwin Brandstetter,2015年

2
我的意思是在同一事务中,而不仅仅是同一会话(因为在tx端释放了锁)。@alexk的答案是我在想的,但是如果tx以该函数开始和结束,则无法解释死锁。
DanielVérité15年

1
@Erwin您无疑会对在pgsql-bugs上发布的答案感兴趣:)
杰克说尝试topanswers.xyz

2
确实非常有趣。有道理,这在plpgsql中也有效,因为我记得类似的plpgsql情况按预期工作。
Erwin Brandstetter

Answers:


10

我张贴这种向pgsql-错误有答复汤姆里表示,这是一个锁升级问题,通过的方式SQL语言功能的机制变相进行处理。本质上,由生成的锁是在表的排他锁之前insert获得的

我认为问题在于,SQL函数将立即对整个函数主体进行解析(也许也正在计划;现在不希望检查代码)。这意味着由于INSERT命令,您在函数主体解析期间(实际上在执行LOCK命令之前)会在“测试”表上获取RowExclusiveLock。因此,LOCK表示尝试进行锁升级,并且会出现死锁。

这种编码技术在plpgsql中是安全的,但在SQL语言函数中则不安全。

已经讨论过重新实现SQL语言函数,以便一次只执行一个语句的解析,但不要屏住呼吸朝着该方向发生的事情。对于任何人来说,这似乎都不是一个高度优先的问题。

问候,汤姆巷

这也解释了为什么在包装的plpgsql块中锁定该函数外部的表(如@ypercube 所建议)可以防止死锁。


3
细点:ypercube实际测试以纯SQL一个锁定在一个显式事务的功能,这是一样的一个PLPGSQL块。
Erwin Brandstetter,2015年

1
完全正确,我不好。我想我对我们尝试过的另一件事感到困惑(这并没有防止僵局)。
杰克说请尝试topanswers.xyz 2015年

4

假设您在调用new_customer之前运行了另一个语句,并且这些语句获得了与之冲突的锁EXCLUSIVE(基本上是对customer表中的任何数据修改),则说明非常简单。

可以用一个简单的示例(甚至不包含函数)重现该问题:

CREATE TABLE test(id INTEGER);

第一场:

BEGIN;

INSERT INTO test VALUES(1);

第二届

BEGIN;
INSERT INTO test VALUES(1);
LOCK TABLE test IN EXCLUSIVE MODE;

第一届

LOCK TABLE test IN EXCLUSIVE MODE;

当第一个会话执行插入操作时,它将获取ROW EXCLUSIVE表上的锁。同时,会话2尝试也获得了ROW EXCLUSIVE锁,并尝试获取了EXCLUSIVE锁。由于EXCLUSIVE锁与冲突,因此此时必须等待第一个会话ROW EXCLUSIVE。最后,第一个会话跳过了鲨鱼并尝试获取EXCLUSIVE锁,但是由于按顺序获取了锁,因此它在第二个会话之后排队。依次等待第一个,从而产生死锁:

DETAIL:  Process 28514 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28084.
Process 28084 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28514

解决此问题的方法是尽早获取锁,通常这是事务中的第一件事。另一方面,PostgreSQL工作负载仅在极少数情况下需要锁定,因此,我建议重新考虑您进行更新的方式(请参阅本文http://www.depesz.com/2012/06/10 / why-is-upsert-so-complicated /)。


2
这一切都很有趣,但是数据库日志中的消息显示为:Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))杰克刚得到:Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))-表示函数调用是两个事务中的第一个命令(除非我遗漏了一些东西)。
Erwin Brandstetter

谢谢,我同意你的意思,但是在这种情况下,这似乎并不是原因。在我添加到该问题的最小测试用例中,这一点更加清楚(您可以尝试一下)。
杰克说请尝试topanswers.xyz 2015年

2
实际上,事实证明您对锁升级是正确的-尽管该机制很微妙
杰克说试试topanswers.xyz 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.