线上死锁解决方法

📅 2026/7/18 17:10:35 👁️ 阅读次数 📝 编程学习
线上死锁解决方法

线上数据库死锁完整排查 + 解决方案(MySQL 为主,通用)

一、先快速定位线上死锁(紧急止损)

1. 立刻查看死锁日志(MySQL)

-- 查看最近一次死锁详情(事务、SQL、锁、等待关系) SHOW ENGINE INNODB STATUS;

重点看LATEST DETECTED DEADLOCK区块:

  • 两个事务分别执行的 SQL
  • 各自持有什么锁、等待什么锁
  • 事务隔离级别、索引命中情况

2. 查看当前阻塞事务

-- 查看活跃事务、锁等待 SELECT * FROM information_schema.innodb_trx; -- 查看锁等待关系 SELECT * FROM information_schema.innodb_locks; SELECT * FROM information_schema.innodb_lock_waits;

3. 紧急恢复线上服务

死锁会自动回滚代价更小的事务,但高并发下会频繁触发报错,临时缓解:

  1. 杀掉长时间阻塞事务:KILL 事务ID;
  2. 临时降并发、限流、关闭高频同步接口
  3. 临时拆分大事务,缩短执行时间

二、死锁根本成因(90% 线上问题根源)

InnoDB 行锁死锁四要素:互斥、持有并等待、不可剥夺、循环等待高频场景:

  1. 多事务更新行顺序不一致(最常见) T1:update user set xx where id=1 → id=2 T2:update user set xx where id=2 → id=1 互相持有对方需要的行锁,循环等待死锁
  2. 无索引 / 索引失效,行锁升级为表锁,大量锁冲突
  3. 大事务:一次更新几十上百行,持有锁时间过长
  4. 隔离级别REPEATABLE READ(默认)存在 Gap 锁 / Next-key 锁,间隙锁冲突
  5. 同一事务多次更新同一张表不同行、批量更新乱序
  6. 读写并发:SELECT ... FOR UPDATE 悲观锁争抢

三、线上根治方案(按优先级落地)

方案 1:统一更新数据顺序(最有效,零成本)

所有业务更新多行时,强制按主键 / 唯一索引升序排序,杜绝循环等待。 错误示例:

// 随机顺序更新,极易死锁 for(Long id : randomIds){ updateById(id); }

正确写法:

// 统一升序,不会出现循环等待 ids.sort(Long::compareTo); for(Long id : ids){ updateById(id); }

批量 SQL 统一排序:

-- 批量更新前先排序ID UPDATE t SET status=1 WHERE id IN (1,3,5) ORDER BY id;

方案 2:缩小事务粒度,缩短锁持有时间

  1. 事务只放 DB 操作,移除 RPC、文件 IO、复杂计算
  2. 大事务拆分:批量更新拆成分批小事务,中间提交释放锁
  3. 避免长事务:查询、业务逻辑移出事务;禁止事务内 sleep

方案 3:保证更新条件命中索引,避免表锁

更新 / 悲观查询必须走唯一索引 / 主键,否则 InnoDB 会锁全表,并发必死锁。

-- 无索引,行锁变表锁,并发大量冲突 UPDATE order SET status=2 WHERE phone='13800000000'; -- 优化:phone加唯一索引,精准行锁

排查索引失效:EXPLAIN update xxx,看 type 是否为 ALL(全表扫描)。

方案 4:规避 Gap 间隙锁冲突(RR 隔离级)

  1. 能改用 RC 读已提交隔离级优先(无 Gap 锁,死锁大幅减少,需确认业务无幻读要求)
    transaction_isolation = READ-COMMITTED
  2. 查询更新使用精确等值主键,不使用范围条件(范围会加间隙锁)
    -- 范围条件产生Gap锁,容易死锁 UPDATE t SET num=1 WHERE id>100; -- 改为批量等值有序更新

方案 5:悲观锁优化,减少锁争抢

  1. 非强一致性场景用乐观锁替代SELECT FOR UPDATE增加 version 版本号:
    UPDATE t SET num=num-1,version=version+1 WHERE id=? AND version=?;
    更新失败重试(控制重试次数,避免风暴)
  2. 必须悲观锁时:统一加锁顺序、缩小锁定行数、尽早提交事务

方案 6:批量操作规范

  1. 禁止一次性更新上千行,分批提交(每批 50~200 条)
  2. 批量 DELETE/UPDATE 强制 ORDER BY 主键排序
  3. 批量插入用 INSERT ... ON DUPLICATE KEY,避免先查后改多行争抢

四、代码层面通用防死锁规范(Java/MyBatis)

  1. 所有多 ID 更新、批量操作,先排序主键再执行
  2. 事务方法尽量短小,禁止外部依赖耗时操作
  3. 悲观锁只锁定必要数据,不要大范围锁定
  4. 乐观锁重试机制:最多 3 次重试,失败抛异常降级
  5. 分布式场景:分布式锁(Redisson)控制行操作串行,避免跨实例死锁

五、线上监控预警(提前发现死锁)

  1. MySQL 开启死锁日志持久化:innodb_print_all_deadlocks=ON
  2. 业务埋点:捕获数据库死锁异常Deadlock found when trying to get lock,告警推送
  3. 监控指标:死锁次数、锁等待时长、长事务数量、慢 SQL
  4. 定时巡检SHOW ENGINE INNODB STATUS采集死锁日志入库分析

六、死锁异常兜底处理(程序降级)

捕获死锁异常后重试(短重试间隔,限制次数):

// 伪代码 int retry = 0; while(retry < 3){ try{ db.transaction(); break; }catch(DeadlockException e){ retry++; Thread.sleep(10); } } if(retry >=3) throw BusinessException("操作繁忙,请重试");

七、快速排查流程总结(线上应急步骤)

  1. 业务报错出现死锁 → 执行SHOW ENGINE INNODB STATUS拿到死锁两条 SQL
  2. 核对两条 SQL 更新 ID 顺序是否相反 → 统一排序修复
  3. EXPLAIN 检查是否无索引导致表锁 → 加索引
  4. 查看事务是否过长 → 拆分大事务
  5. 范围更新过多 Gap 锁 → 切换 RC 隔离或改用等值批量有序更新
  6. 临时限流止损,上线修复代码后观察死锁告警是否归零