多进程插入重复行触发PostgreSQL事务死锁问题求助
嘿,这个死锁问题我之前在PostgreSQL 9.x版本里碰见过,尤其是带唯一约束但非主键的并发插入场景,咱们一步步来拆解根源和解决办法。
为什么会触发死锁?
当多个进程同时尝试插入相同唯一键值的行时,PostgreSQL的锁机制会出现循环等待的情况:
- 进程A先尝试获取唯一索引上的锁,之后等待表的行锁;
- 进程B先拿到了表的行锁,接着等待唯一索引的锁;
- 两边互相等待对方释放锁,就形成了死锁,最终数据库会终止其中一个事务来打破循环。
而且你这里的唯一约束不是主键——主键的锁处理有PostgreSQL的优化逻辑,但非主键的唯一约束在并发场景下更容易出现这种锁等待顺序不一致的问题,尤其是在9.6这个版本里。
具体解决方案(按推荐优先级排序)
1. 用INSERT ... ON CONFLICT原子操作(最推荐)
PostgreSQL 9.5及以上版本支持ON CONFLICT子句,这是官方专门为解决并发唯一约束插入问题设计的原子操作,数据库层面会自动处理锁的顺序,从根源上减少死锁概率,而且性能比先查后插更高。
在SQLAlchemy里可以这么写:
from sqlalchemy.dialects.postgresql import insert # 构造插入语句,冲突时什么都不做 stmt = insert(YourModel.__table__).values(unique_col=target_value, other_col=your_data) stmt = stmt.on_conflict_do_nothing(index_elements=['unique_col']) # 指定唯一约束的列 session.execute(stmt) session.commit()
如果冲突时需要更新其他字段,改成on_conflict_do_update即可:
stmt = insert(YourModel.__table__).values(unique_col=target_value, other_col='new_content') stmt = stmt.on_conflict_do_update( index_elements=['unique_col'], set_={'other_col': stmt.excluded.other_col} # 用插入的值更新现有行 ) session.execute(stmt) session.commit()
你的环境(PostgreSQL 9.6.8 + SQLAlchemy 1.1.15 + psycopg2 2.7.3.2)完全支持这个语法,不需要升级依赖就能直接用。
2. 先查询锁定再插入(兼容旧版本场景)
如果因为某些原因不能用ON CONFLICT,可以在同一个事务里先查询并锁定目标行,确认不存在再插入:
with session.begin(): # 用with_for_update锁定该行,防止其他进程同时插入 existing_row = session.query(YourModel)\ .filter(YourModel.unique_col == target_value)\ .with_for_update()\ .first() if not existing_row: new_row = YourModel(unique_col=target_value, ...) session.add(new_row)
这个方法能避免盲插带来的锁冲突,但每次插入都要先执行一次查询,性能会比ON CONFLICT稍差一些。
3. 查看死锁日志精准定位
PostgreSQL会把死锁的详细信息记录在日志里,你可以去/var/lib/pgsql/9.6/data/pg_log/目录下找最新的日志文件,里面会显示死锁发生时的进程锁持有/等待关系,能帮你确认是不是唯一约束的锁导致的循环等待,也方便你排查有没有其他隐藏的锁冲突点。
4. 调整事务隔离级别(谨慎使用)
如果当前用的是默认的READ COMMITTED隔离级别,可以尝试改成REPEATABLE READ,这个级别下事务会持有锁直到结束,能避免一些锁顺序不一致的问题,但会增加锁的持有时间,可能带来其他性能瓶颈,所以只推荐在其他方法无效时尝试。
总结
最优先推荐用INSERT ... ON CONFLICT,这是PostgreSQL官方给出的最优解,既能避免死锁,又保证了操作的原子性和性能。如果必须兼容更老的版本,再考虑先查后插加锁的方式。
内容的提问来源于stack exchange,提问作者shaffooo

