mysql行锁如何避免死锁_mysql并发事务处理

2次阅读

死锁主因是加锁顺序不一致而非锁多;需统一按主键升序访问数据、慎用非唯一索引、缩小事务粒度、捕获重试死锁并加强线上监控。

mysql行锁如何避免死锁_mysql并发事务处理

死锁不是锁多了,而是加锁顺序不一致

mysql 的行锁本身不会导致死锁,真正触发死锁的是多个事务以不同顺序访问同一组行。比如事务 A 先更新 id=1 再更新 id=5,事务 B 反过来先更新 id=5 再更新 id=1,InnoDB 检测到循环等待就会主动回滚其中一个事务,并报错:Deadlock found when trying to get lock

避免的核心思路是:让所有事务按相同逻辑顺序访问数据。

  • 按主键升序批量更新:对要操作的 ID 列表先 ORDER BY id 排序,再逐条或批量处理
  • 避免在应用层分批查再更新:比如先 select ... for UPDATE 查出 10 条,再循环更新——这容易因查询结果顺序与实际索引扫描顺序不一致埋下隐患
  • 尽量用单条 UPDATE ... WHERE id IN (1,5,3) 替代多条语句,InnoDB 内部会对 IN 列表按主键排序后加锁

唯一索引 vs 非唯一索引,加锁范围差别很大

同一个 WHERE 条件,在不同索引类型下,InnoDB 加的锁可能从“仅目标行”扩大到“间隙锁”甚至“临键锁”,直接影响并发行为和死锁概率。

例如执行 UPDATE users SET name='x' WHERE age=25

  • 如果 age 是唯一索引:只锁匹配的行(record lock)
  • 如果 age 是普通索引:会加临键锁(next-key lock),即锁住满足 age=25 的所有行 + 这些行之间的间隙,可能意外阻塞其他事务插入或更新 nearby 数据
  • 如果 age 没有索引:走全表扫描,每行都加 record lock,锁粒度最大,死锁风险显著上升

查死锁日志时注意看 SHOW ENGINE INNODB STATUS 输出里的 *** (1) WAITING FOR this LOCK TO BE GRANTED:*** (2) HOLDS THE LOCK(S):,能清楚看到各自持有哪些锁、等待哪把锁。

事务粒度太大,是死锁温床

一个事务里混着 SELECT、INSERT、UPDATE、外部 API 调用、复杂计算,持续时间越长,持有锁的时间就越长,和其他事务冲突的概率就越高。

  • 把非数据库操作(如发邮件、调第三方服务)移出事务块,改用消息队列或异步任务
  • 避免在事务中做耗时循环或大对象序列化/反序列化
  • 读多写少场景下,考虑用 SELECT ... LOCK IN SHARE MODE 替代 FOR UPDATE,降低锁强度
  • 确认是否真的需要可重复读(RR)隔离级别;部分业务用 READ COMMITTED 能减少间隙锁,但要注意幻读是否可接受

监控和快速定位比预防更现实

完全杜绝死锁几乎不可能,尤其在高并发、多服务协作的系统里。比起花大量精力设计“绝对安全”的 SQL,不如建立快速响应机制:

  • 应用层捕获 Deadlock found when trying to get lock 错误,自动重试(带退避,比如 10ms、50ms、200ms)
  • 定期跑 SHOW ENGINE INNODB STATUS 并提取最近死锁信息,用脚本聚合分析高频冲突的表和条件
  • 在慢日志或性能 schema 中开启 innodb_print_all_deadlocks = ON,确保每次死锁都落盘,不依赖手动触发 SHOW

最常被忽略的一点:开发环境很少压测复合事务路径,而线上真实请求链路往往跨多个微服务、多次 DB 调用,锁的累积效应只有在流量高峰才暴露。别只盯着单条 SQL 是否加锁合理。

text=ZqhQzanResources