我正在构建一个基于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
适用于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
仅当目标行locked
为false
时,才应获取目标行上的锁定。但实际上,它不是那样的。不知何故,即使没有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
答案 0 :(得分:6)
测试用例如下:
SELECT
并获取记录锁。SELECT
并进入锁的等待队列。UPDATE
/ COMMIT
并释放锁。SELECT
以来已更改,它会根据WHERE
条件重新检查数据。检查失败,并且该行已从结果集中过滤掉,但仍然保持锁定。 FOR UPDATE
documentation中提到了此行为:
...在查询快照中满足查询条件的行将被锁定,但如果在快照之后更新它们并且不再满足查询条件,则不会返回它们。
这可能有一些unpleasant consequences,所以多余的锁不是 坏,所有事情都要考虑。
可能最简单的解决方法是通过在acquire
的每次迭代后提交来限制锁定持续时间。还有其他各种方法可以防止它持有此锁定(例如SELECT ... NOWAIT
,在Postgres 9.5中以REPEATABLE READ
或SERIALIZABLE
隔离级别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代码,并在您显示的屏幕截图中,在我看来:
locked=true
锁,并且为waiting to acquire a row lock。locked=true
锁定。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时),并添加导致死锁的几率。