SELECT FOR UPDATE的奇怪死锁PostgreSQL死锁问题

Fan*_*Lin 4 sql database postgresql deadlock transactions

我正在构建一个基于PostgreSQL的锁定系统,我有两种方法,acquire并且release.

因为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
Run Code Online (Sandbox Code Playgroud)

并为 release

BEGIN
UPDATE my_locks SET locked = false WHERE id = '<NAME>'
COMMIT
Run Code Online (Sandbox Code Playgroud)

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

SELECT id FROM my_locks WHERE locked = false AND id = '<NAME>' FOR UPDATE
Run Code Online (Sandbox Code Playgroud)

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

选择更新死锁问题

释放正在等待SELECT FOR UPDATE,并且SELECT FOR UPDATE正在进行无限循环,同时它无缘无故地保持锁定.

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

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

您可以使用psycopg2和运行它pytest,记得更改数据库设置,然后运行

pip install pytest psycopg2
py.test -sv test_lock.py
Run Code Online (Sandbox Code Playgroud)

Nic*_*nes 7

测试用例如下:

  • Thread-1运行SELECT并获取记录锁.
  • Thread-2运行SELECT并进入锁的等待队列.
  • Thread-1运行UPDATE/ COMMIT并释放锁.
  • Thread-2获取锁定.检测到记录自其更改后SELECT,它会根据其WHERE状况重新检查数据.检查失败,并从结果集中过滤掉行,但仍保持锁定.

FOR UPDATE文档中提到了此行为:

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

这可能会产生一些令人不快的后果,因此所有事情都考虑了多余的锁定并不是那么糟糕.

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

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

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

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

最简单的方法可能是实现acquireas SELECT FROM my_locks FOR UPDATE,releaseas COMMIT,并让进程争用行锁.如果您需要更多灵活性(例如阻塞/非阻塞调用,事务/会话/自定义范围),咨询锁应该证明是有用的.