MySQL的REPEATABLE READ易引发死锁,因其默认使用间隙锁+记录锁组合,对索引范围加锁;若事务按不同顺序更新重叠数据(如A先id=5后id=3、B相反),便形成循环等待,导致Deadlock异常。

死锁不是事务隔离级别设高了就自动出现的,而是隔离级别 + 并发操作模式 + 数据库锁机制共同作用的结果。盲目调高 isolation_level 反而可能加剧死锁,尤其在 PostgreSQL 和 MySQL 的默认行为差异下。
为什么 REPEATABLE READ 在 MySQL 下容易引发死锁
MySQL(InnoDB)的 REPEATABLE READ 默认使用间隙锁(gap lock)+ 记录锁(record lock)组合,对 WHERE 条件涉及的索引范围加锁,哪怕那行数据还不存在。两个事务若按不同顺序更新重叠范围(比如事务 A 先更新 id=5 再更新 id=3,事务 B 反过来),就极易形成循环等待。
- 现象:
Deadlock found when trying to get lock; try restarting transaction - 典型场景:库存扣减、积分转账、并发生成唯一编号
- 对比 PostgreSQL:
REPEATABLE READ实际是快照隔离(SI),不加间隙锁,死锁概率显著更低 - 解决思路:优先用
READ COMMITTED+ 显式SELECT ... FOR UPDATE控制锁粒度,而不是依赖默认锁行为
isolation_level 在 Flask-SQLAlchemy 中的实际生效位置
配置项写在 SQLALCHEMY_ENGINE_OPTIONS 里,只影响新建立的连接;已存在的连接、已开启的事务不会被动态修改。很多开发者误以为改了配置就能“立刻生效”,其实只是下次连接才用新级别。
- Flask 应用启动时创建引擎,此后所有连接复用该配置
-
db.session本身不携带隔离级别,它依赖底层连接;db.session.begin()不会覆盖连接的isolation_level - 若需临时提升某次操作的隔离强度,必须用原生 SQL 执行
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE,且必须在conn.begin()前调用 - SQLite 是个例外:它的
SERIALIZABLE实际是全局排他锁,高并发下直接变单线程,慎用
如何验证当前连接实际使用的隔离级别
不能只信配置,要查数据库真实反馈。不同数据库查法不同,但都得绕过 ORM 直接发查询。
立即学习“Python免费学习笔记(深入)”;
- PostgreSQL:
SELECT current_setting('transaction_isolation') - MySQL:
SELECT @@tx_isolation或SELECT @@transaction_isolation(8.0+) - 执行时机很重要:必须在事务内查,否则返回的是会话默认值,不是当前事务实际生效值
- 推荐封装一个调试工具函数,在关键事务开头执行并记录日志,避免线上出问题后靠猜
真正难处理的不是“怎么设隔离级别”,而是当多个服务共用同一张表、或触发器/存储过程介入时,锁行为会脱离 SQLAlchemy 的控制范围——这时候看日志、抓 pg_locks 或 INFORMATION_SCHEMA.INNODB_TRX 比调参数更管用。


















