大事务在REPEATABLE READ下易引发死锁,因其采用Next-Key Lock导致锁范围广、时间长;改用READ COMMITTED可减小锁粒度和持有时间,但不解决锁序问题且引入不可重复读;应调低innodb_lock_wait_timeout至10~30秒,并通过拆分事务、固定锁序、索引优化及移出外部操作来治本。

大事务不是不能做,但默认 REPEATABLE READ + 长时间持有行锁,等于主动往死锁堆里跳。 MySQL 默认隔离级别下,一个更新语句可能锁住不止匹配行,还带上间隙(Next-Key Lock),事务越长,锁住的范围越广、时间越久,和其他事务撞锁的概率就指数级上升。
为什么 REPEATABLE READ 在大事务里特别危险
这不是隔离级别本身有问题,而是它在 InnoDB 中的实现机制和大事务天然冲突:
-
REPEATABLE READ下,普通SELECT走 MVCC 快照读不加锁,但所有UPDATE/DELETE都会用 Next-Key Lock —— 既锁记录,又锁间隙,防止幻读 - 一个耗时 2 秒的大事务执行
UPDATE ... WHERE status = 'pending',可能锁住几百行+它们之间的所有间隙;期间另一个事务想插一条新 pending 记录,或更新其中某一行,就会被卡住 - 若此时第二个事务也正持有一批锁并反过来等第一个事务释放,死锁立刻触发
- MySQL 的死锁检测是事后机制(发现循环等待才回滚一个),不是预防手段
改用 READ COMMITTED 能解决什么、不能解决什么
这是最常用、见效最快的一线调整,但得清楚边界:
- ✅ 锁粒度变小:
READ COMMITTED下,UPDATE只锁真正命中的行(Record Lock),不锁间隙,大幅降低“误伤”概率 - ✅ 锁持有时间变短:每次语句执行完就释放锁(非事务结束才放),减少锁等待窗口
- ❌ 不解决锁顺序混乱问题:如果两个事务仍以不同顺序更新
user_id=100和user_id=200,照样死锁 - ❌ 业务需接受不可重复读:同一事务内两次
SELECT可能拿到不同结果(别人已提交),比如查余额后扣款,中间被充值了,你扣的是旧余额
实操建议:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; 或在应用层连接初始化时统一设置,避免单条 SQL 临时改级别引发行为不一致。
innodb_lock_wait_timeout 不是防死锁,是控影响面
这个参数管的是“等锁超时”,不是“死锁超时”。MySQL 检测到死锁是立刻处理(选 victim 回滚),而锁等待超时是另一回事:
- 默认值通常是
50秒,意味着一个事务卡在等锁上最多忍 50 秒才报错Lock wait timeout exceeded - 对大事务来说,50 秒太长 —— 用户请求早超时了,后台还在空转占资源。建议调低到
10~30秒,让失败更快暴露 - 别设成
1:网络抖动、瞬时锁竞争都可能误触发,反而增加重试压力 - 注意:该参数只影响当前会话,线上应通过
my.cnf的innodb_lock_wait_timeout = 15全局设置,并重启生效
真正治本:拆事务 + 固定锁序 + 索引覆盖
隔离级别和超时只是缓冲垫,核心还得从代码逻辑下手:
- 把一个 50 行的批量更新事务,拆成每批 100 条的循环事务,每次只锁少量数据,中间 commit 释放全部锁
- 所有涉及多主键更新的逻辑,强制按主键升序(如
ORDER BY id ASC)更新,确保所有服务写法一致 —— 这比任何配置都管用 - 检查
WHERE条件字段是否都有索引:没索引会导致全表扫描+全表加锁,status = 'pending'如果没索引,整个表都可能被锁住 - 用
SELECT ... FOR UPDATE显式加锁时,务必加在最终要改的同一张表、同一组条件上,避免先查后更导致锁升级或范围扩大
最常被忽略的一点:事务里混入了 HTTP 调用、文件读写、消息队列发包等外部操作。这些不参与数据库锁管理,却把锁持有时间拉长数秒甚至分钟 —— 这类操作必须挪到事务外。

















