我注意到同时执行简单且相同的查询:
BEGIN;
SELECT items.item_id AS items_item_id
FROM items
WHERE items.item_id = 1 FOR UPDATE;
COMMIT;
导致死锁,这使我感到惊讶,因为看起来这样的查询不应创建死锁。
此测试用例的表和数据定义为:
CREATE TABLE items (
item_id serial PRIMARY KEY
);
insert into items Values (1);
通过SQLAlchemy
执行上述SQL语句的Python代码为:
item = DB.query(Item).filter( Item.item_id == 1).with_for_update().one()
time.sleep(0.001)
DB.commit()
SQLALchemy Item
表定义:
class Item(Base):
__tablename__ = 'items'
item_id = Column(Integer, primary_key=True)
在这种死锁情况下,如果我运行:
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
当同一条SELECT
语句彼此依赖时,我将得到循环依赖。这是典型的输出(请注意24869
和24868
相互依赖):
blocked_pid | blocking_pid | blocked_statement | current_statement_in_blocking_process
-------------+--------------+----------------------------------------+----------------------------------------
24867 | 24865 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24868 | 24867 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24870 | 24867 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24871 | 24867 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24869 | 24867 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24870 | 24868 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24871 | 24868 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24869 | 24868 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24868 | 24869 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24870 | 24869 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24871 | 24869 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24868 | 24870 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24871 | 24870 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24869 | 24870 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24868 | 24871 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24870 | 24871 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
24869 | 24871 | SELECT items.item_id AS items_item_id +| SELECT items.item_id AS items_item_id +
| | FROM items +| FROM items +
| | WHERE items.item_id = 1 +| WHERE items.item_id = 1 +
| | LIMIT 1 FOR UPDATE | LIMIT 1 FOR UPDATE
(17 rows)
所以我的问题是:
这样的行为是否正常,应该可以预料,或者我的代码中有错误(或者可能是SQLAlchemy / PostgreSQL)?
如果这种行为是正常的和预期的:为什么会发生这种情况?特别是,我的印象是FOR UPDATE
应该是原子的并且不能相互依赖(这个正确的假设吗?)
是否可以在不使用NO WAIT
中的SKIP LOCKED
或FOR UPDATE
的情况下以某种方式修改应用程序代码以避免此类死锁?
编辑:将错过的BEGIN; ... COMMIT;
添加到代码中
答案 0 :(得分:0)
如果我对您的理解正确,那么您正在执行两个并发查询,您在SELECT FOR UPDATE
处对项目表中的第一项进行了查询?是的,那绝对会导致死锁。从文档中:
FOR UPDATE导致SELECT语句检索的行被锁定,就像更新一样。 [...] 也就是说,尝试对这些行进行UPDATE,DELETE或SELECT FOR UPDATE的其他事务将被阻止,直到当前事务结束为止。
请参见另一个问题here,我相信它涵盖的问题与您的问题类似,当然还要减去子查询的问题。如果执行两个查询以选择并显式锁定同一行,则第一个查询可能成功选择并锁定了该行,第二个查询被阻塞,直到解除锁定为止,然后当执行第一个查询时,第二个查询中的块抬起,然后选择并锁定该行。请记住,查询未序列化。