SELECT FOR UPDATE

时间:2015-08-06 08:26:49

标签: sql database postgresql transactions deadlock

我正在构建一个基于PostgreSQL的锁定系统,我有两种方法,acquirerelease

acquire,就像这样

BEGIN
while True:
    SELECT id FROM my_locks WHERE locked = false AND id = '<NAME>' FOR UPDATE
    if no rows return:
        continue
    UPDATE my_locks SET locked = true WHERE id = '<NAME>'
    COMMIT
    break

适用于release

BEGIN
UPDATE my_locks SET locked = false WHERE id = '<NAME>'
COMMIT

这看起来非常简单,但它不起作用。奇怪的是,我想是

SELECT id FROM my_locks WHERE locked = false AND id = '<NAME>' FOR UPDATE
仅当目标行lockedfalse时,

才应获取目标行上的锁定。但实际上,它不是那样的。不知何故,即使没有locked = false行存在,它仍会获得锁定。结果,我遇到了死锁问题。看起来像这样

Select for update dead lock issue

发布正在等待SELECT FOR UPDATE,而SELECT FOR UPDATE正在进行无限循环,同时它无缘无故地锁定。

为了重现这个问题,我在这里写了一个简单的测试

https://gist.github.com/victorlin/d9119dd9dfdd5ac3836b

您可以使用psycopg2pytest运行它,请记住更改数据库设置,然后运行

pip install pytest psycopg2
py.test -sv test_lock.py

3 个答案:

答案 0 :(得分:6)

测试用例如下:

  • Thread-1运行SELECT并获取记录锁。
  • Thread-2运行SELECT并进入锁的等待队列。
  • Thread-1运行UPDATE / COMMIT并释放锁。
  • 线程-2获取锁定。检测到记录自SELECT以来已更改,它会根据WHERE条件重新检查数据。检查失败,并且该行已从结果集中过滤掉,但仍然保持锁定。

FOR UPDATE documentation中提到了此行为:

  

...在查询快照中满足查询条件的行将被锁定,但如果在快照之后更新它们并且不再满足查询条件,则不会返回它们。

这可能有一些unpleasant consequences,所以多余的锁不是 坏,所有事情都要考虑。

可能最简单的解决方法是通过在acquire的每次迭代后提交来限制锁定持续时间。还有其他各种方法可以防止它持有此锁定(例如SELECT ... NOWAIT,在Postgres 9.5中以REPEATABLE READSERIALIZABLE隔离级别SELECT ... SKIP LOCKED运行。“

我认为使用此重试循环方法的最干净的实现是完全跳过SELECT,并且只运行UPDATE ... WHERE locked = false,每次都提交。您可以在致电cur.rowcount后查看cur.execute(),了解您是否获得了锁定。如果您需要从锁记录中提取其他信息,则可以使用UPDATE ... RETURNING语句。

但我必须同意@Kevin,并说你可能会更好地利用Postgres的内置锁定支持,而不是试图重新发明它。它会为你解决很多问题,例如:

  • 自动检测到死锁
  • 等待进程进入休眠状态,而不是必须轮询服务器
  • 锁定请求排队,防止饥饿
  • 锁定(通常)不会比失败的过程更长久

最简单的方法可能是将acquire简化为SELECT FROM my_locks FOR UPDATE,将release简化为COMMIT,然后让进程争用行锁。如果您需要更多灵活性(例如阻止/非阻止调用,事务/会话/自定义范围),advisory locks应该证明是有用的。

答案 1 :(得分:2)

PostgreSQL normally中止死锁的交易:

  

使用显式锁定可以增加死锁的可能性,其中两个(或更多)事务每个都持有另一个想要的锁。例如,如果事务1获取表A上的独占锁,然后尝试获取表B上的独占锁,而事务2已经具有独占锁定表B并且现在想要对表A进行独占锁定,那么任何人都无法继续。 PostgreSQL会自动检测死锁情况,并通过中止其中一个交易来解决这些问题,允许其他人完成。 (究竟哪个交易将被中止很难预测,不应该依赖。)

查看您的Python代码,并在您显示的屏幕截图中,在我看来:

  • 主题3持有locked=true锁,并且为waiting to acquire a row lock
  • 线程1也在等待行锁定,还有locked=true锁定。
  • 唯一合乎逻辑的结论是,线程2以某种方式持有行锁,并等待locked=true锁定(请注意该查询的短时间;它是循环,而不是阻塞)。

由于Postgres不知道locked=true锁定,因此在这种情况下无法中止事务以防止死锁。

我不能立即清楚T2是如何获得行锁定的,因为我所看到的所有信息都是it can't do that

  

FOR UPDATE导致 SELECT语句检索的行被锁定,就像更新一样。这可以防止它们被其他事务锁定,修改或删除,直到当前事务结束。也就是说,在当前事务结束之前,将阻止尝试UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE,SELECT FOR SHARE或SELECT FOR KEY FOR KEY SHARE这些行的其他事务;相反,SELECT FOR UPDATE将等待在同一行上运行任何这些命令的并发事务,然后锁定并返回更新的行(如果行被删除则不返回行) 。但是,在REPEATABLE READ或SERIALIZABLE事务中,如果要锁定的行自事务启动以来已更改,则将引发错误。有关进一步的讨论,请参阅第13.4节。

我无法找到任何证据表明PostgreSQL“神奇地”将行锁升级为表锁或任何类似的东西。

但你所做的事情也显然不安全。您正在获取锁A(行锁),然后获取锁B(显式locked=true锁定),然后释放并重新获取A,然后按顺序释放B和A.这不能正确地观察锁定层次结构,因为我们尝试在保持B和反之亦然的同时获取A。但OTOH,在持A的情况下获得B不应该失败(我认为),所以我仍然不确定这是完全错误的。

坦率地说,我认为你最好只在空表上使用LOCK TABLE语句。 Postgres知道这些锁并将为您检测死锁。它还为您节省了SELECT FOR UPDATE finagling的麻烦。

答案 2 :(得分:1)

此外,您应在发布代码中添加locked = true

BEGIN
UPDATE my_locks SET locked = false WHERE id = '<NAME>' AND locked = true
COMMIT

如果没有,您正在更新记录的任何锁定状态(在您的情况下,即使在locked = false时),并添加导致死锁的几率。