📌PDF:大白话说Java面试题 — 03-Mysql篇
第25题:MySQL 遇到过死锁问题吗?你是如何解决的
📚回答:
- 核心考点:
大厂面试要求不仅掌握死锁的基本概念,更要深入理解InnoDB死锁检测机制、常见死锁场景、如何定位死锁根因,以及生产环境的预防和解决方案。面试官常追问:“死锁日志怎么看?”、“如何在不改代码的情况下缓解死锁?”、“间隙锁引起的死锁怎么解决?”
1. 死锁的核心概念
定义:两个或多个事务互相持有对方需要的锁,形成循环等待,导致所有事务都无法继续执行。
四个必要条件(缺一不可):
| 条件 | 说明 |
|---|---|
| 互斥 | 资源一次只能被一个事务持有 |
| 持有并等待 | 事务持有锁的同时等待其他锁 |
| 不可剥夺 | 已持有的锁不能被强制剥夺 |
| 循环等待 | 事务间形成环路等待 |
InnoDB死锁处理机制:
- 自动死锁检测:维护事务等待图(Waits-for Graph),周期性检测循环依赖
- 自动回滚:选择回滚代价较小的事务(修改行数少的)
- 参数控制:
innodb_deadlock_detect = ON(默认开启)
2. 常见死锁场景
2.1 交叉加锁(最经典)
-- 事务ASTARTTRANSACTION;UPDATEaccountSETbalance=balance-100WHEREid=1;-- 锁住id=1UPDATEaccountSETbalance=balance+100WHEREid=2;-- 等待id=2COMMIT;-- 事务BSTARTTRANSACTION;UPDATEaccountSETbalance=balance-100WHEREid=2;-- 锁住id=2UPDATEaccountSETbalance=balance+100WHEREid=1;-- 等待id=1COMMIT;结果:事务A持有1等待2,事务B持有2等待1 → 死锁
解决方案:固定加锁顺序(先锁id小,再锁id大)
2.2 主键范围查询 + 插入(间隙锁死锁)
-- 事务ASTARTTRANSACTION;SELECT*FROMusersWHEREidBETWEEN10AND20FORUPDATE;-- 加Next-Key Lock-- 间隙锁锁住(10,20)范围INSERTINTOusers(id,name)VALUES(15,'new');-- 等待事务B释放间隙锁-- 事务BSTARTTRANSACTION;SELECT*FROMusersWHEREidBETWEEN10AND20FORUPDATE;-- 同样加Next-Key LockINSERTINTOusers(id,name)VALUES(16,'new');-- 等待事务A释放间隙锁结果:两个事务互相等待对方释放间隙锁
解决方案:降低隔离级别为RC(无间隙锁),或使用唯一索引避免间隙锁
2.3 不同索引加锁顺序不一致
-- 表结构:id(主键), name(普通索引)-- 事务AUPDATEusersSETstatus=1WHEREname='Alice';-- 锁二级索引name,再锁聚簇索引-- 事务BUPDATEusersSETstatus=2WHEREid=100;-- 锁聚簇索引-- 如果事务A和事务B操作的是同一行,加锁顺序不一致可能死锁解决方案:统一先通过主键操作,或使用唯一索引
2.4 批量操作顺序不一致
-- 事务A: DELETE FROM orders WHERE id IN (1,2) ORDER BY id;-- 事务B: DELETE FROM orders WHERE id IN (2,1) ORDER BY id;解决方案:应用层排序后再执行(ORDER BY id)
2.5 外键约束引起的死锁
子表插入时,会检查父表记录并加锁,容易引发死锁
解决方案:简化外键约束,在应用层维护关联关系
3. 如何定位死锁根因
3.1 查看死锁日志
SHOWENGINEINNODBSTATUS\G-- 查看 LATEST DETECTED DEADLOCK 部分死锁日志解读:
------------------------LATEST DETECTED DEADLOCK------------------------***(1)TRANSACTION:TRANSACTION12345,ACTIVE5sec mysqltablesinuse1,locked1LOCKWAIT2lockstruct(s),heap size1136,1rowlock(s)MySQL thread id1,OS thread handle12345,query id123localhost root updatingUPDATEaccountSETbalance=100WHEREid=2-- 关键:事务1在等待id=2的行锁***(1)WAITINGFORTHISLOCKTOBE GRANTED: RECORD LOCKS space id123pageno4n bits72indexPRIMARYoftable`test`.`account`trx id12345lock_mode X locks rec butnotgap waiting***(2)TRANSACTION:TRANSACTION12346,ACTIVE4sec3lockstruct(s),heap size1136,2rowlock(s)MySQL thread id2,OS thread handle12346,query id124localhost root updatingUPDATEaccountSETbalance=200WHEREid=1-- 事务2持有id=1,等待id=2***(2)HOLDS THELOCK(S): RECORD LOCKS space id123pageno4n bits72indexPRIMARYoftable`test`.`account`trx id12346lock_mode X locks rec butnotgap***(2)WAITINGFORTHISLOCKTOBE GRANTED: RECORD LOCKS space id123pageno4n bits72indexPRIMARYoftable`test`.`account`trx id12346lock_mode X locks rec butnotgap waiting***WE ROLL BACKTRANSACTION(1)-- InnoDB回滚了事务1(代价较小的)3.2 监控工具
| 命令/工具 | 作用 |
|---|---|
SHOW ENGINE INNODB STATUS | 查看最近死锁信息 |
SELECT * FROM information_schema.INNODB_TRX | 查看当前运行的事务 |
SELECT * FROM information_schema.INNODB_LOCKS | 查看当前锁信息 |
SELECT * FROM information_schema.INNODB_LOCK_WAITS | 查看锁等待关系 |
| pt-deadlock-logger(Percona Toolkit) | 持续监控死锁并记录 |
4. 死锁解决方案
4.1 立即处理(线上死锁发生时)
| 操作 | 方法 | 风险 |
|---|---|---|
| 查看日志 | SHOW ENGINE INNODB STATUS分析死锁原因 | 无 |
| kill阻塞事务 | KILL <thread_id>(从INNODB_TRX中找) | 业务受影响 |
| 重启应用 | 释放所有连接,清空锁 | 业务中断 |
| 临时降级 | SET GLOBAL innodb_deadlock_detect=OFF(不推荐) | 死锁会变成长时间等待 |
4.2 长期预防
方案一:固定加锁顺序(最有效)
-- 错误:顺序不一致-- 事务A: UPDATE t1 → UPDATE t2-- 事务B: UPDATE t2 → UPDATE t1-- 正确:统一顺序(如先t1后t2)-- 事务A: UPDATE t1 → UPDATE t2-- 事务B: UPDATE t1 → UPDATE t2方案二:缩小事务范围
-- 错误:长事务,锁持有时间长STARTTRANSACTION;SELECT...FORUPDATE;-- 锁1-- 复杂业务逻辑(网络调用、文件操作)UPDATE...;-- 锁2COMMIT;-- 正确:拆分事务,减少锁持有时间-- 先完成业务逻辑(不锁数据库)-- 再用短事务更新STARTTRANSACTION;UPDATE...;COMMIT;方案三:使用更低隔离级别
-- 将隔离级别降为READ COMMITTED(无间隙锁)SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;方案四:唯一索引代替间隙锁
-- 避免使用范围查询,改用唯一索引等值查询-- 错误:范围查询导致间隙锁SELECT*FROMusersWHEREageBETWEEN20AND30FORUPDATE;-- 正确:使用唯一索引等值查询SELECT*FROMusersWHEREid=123FORUPDATE;方案五:应用层重试机制
@Retryable(value=DeadlockException.class,maxAttempts=3,backoff=@Backoff(delay=100))publicvoidupdateAccount(){// 业务逻辑,死锁时自动重试}方案六:调整参数
| 参数 | 作用 | 推荐值 |
|---|---|---|
innodb_deadlock_detect | 死锁检测开关 | ON(默认) |
innodb_lock_wait_timeout | 锁等待超时(秒) | 50(默认) |
innodb_deadlock_detect设为OFF时 | 靠超时释放 | 需配合innodb_lock_wait_timeout |
5. 实战案例分析
案例1:电商下单死锁
场景:用户下单,同时扣减库存和余额
-- 事务A(扣库存)UPDATEproductSETstock=stock-1WHEREid=100;UPDATEaccountSETbalance=balance-100WHEREuser_id=1;-- 事务B(同一用户下单)UPDATEproductSETstock=stock-1WHEREid=101;UPDATEaccountSETbalance=balance-100WHEREuser_id=1;分析:事务A锁产品100+用户1,事务B锁产品101+用户1,无交叉,不会死锁。
真正死锁场景:
-- 事务A:扣库存1 → 扣用户1余额-- 事务B:扣用户1余额 → 扣库存1-- 此时死锁解决方案:固定顺序(先扣库存,再扣余额)
案例2:秒杀系统死锁
场景:高并发秒杀,多个事务同时扣减同一商品库存
UPDATEproductSETstock=stock-1WHEREid=100ANDstock>0;分析:更新同一行,不会死锁(行锁排队),但可能出现锁等待超时。
真正死锁场景:使用SELECT FOR UPDATE先查询再加锁,可能导致间隙锁死锁
解决方案:使用乐观锁(版本号)或UPDATE ... WHERE单条SQL
案例3:订单状态批量更新死锁
场景:批量更新订单状态
-- 事务AUPDATEordersSETstatus=2WHEREstatus=1ANDcreate_time<'2024-01-01';-- 事务B(同时执行,条件不同但可能有重叠)UPDATEordersSETstatus=2WHEREstatus=1ANDcreate_time<'2024-01-02';分析:两个事务扫描不同的范围,加锁顺序不一致(索引(status, create_time)),可能死锁
解决方案:分批执行,每批加ORDER BY id保证顺序
6. 面试官追问与高分回答
Q1:死锁检测对性能有影响吗?
A:有。高并发场景下,死锁检测需要遍历等待图,复杂度O(n²),n为等待事务数。当大量事务并发时,死锁检测可能成为性能瓶颈。MySQL 8.0引入了innodb_deadlock_detect参数,可关闭死锁检测,靠innodb_lock_wait_timeout兜底。
Q2:如何在不改代码的情况下缓解死锁?
A:
- 降低隔离级别(RC无间隙锁)
- 增大
innodb_lock_wait_timeout(让死锁变成超时回滚) - 关闭死锁检测(
innodb_deadlock_detect=OFF),靠超时处理(不推荐) - 优化索引,避免全表扫描(表锁)
Q3:什么情况下InnoDB会选择回滚哪个事务?
A:选择代价较小的事务(修改行数少的)。修改行数相同时,选择后执行的事务。
Q4:间隙锁如何导致死锁?
A:两个事务同时执行SELECT ... WHERE id BETWEEN 10 AND 20 FOR UPDATE,都加间隙锁,然后都尝试插入数据到间隙中,互相等待对方释放间隙锁,导致死锁。解决:使用唯一索引(降级为Record Lock)或RC隔离级别。
Q5:如何避免死锁?
A:
- 固定加锁顺序(最重要的原则)
- 使用唯一索引等值查询(避免间隙锁)
- 缩小事务范围,减少锁持有时间
- 批量操作时
ORDER BY id统一顺序 - 使用乐观锁(版本号)替代悲观锁
Q6:死锁和锁等待超时有什么区别?
A:
- 死锁:循环等待,InnoDB自动检测并回滚一个事务
- 锁等待超时:单方向等待,等待时间超过
innodb_lock_wait_timeout(默认50秒)后,等待者主动放弃
7. 总结对比表
| 死锁原因 | 典型场景 | 解决方案 |
|---|---|---|
| 交叉加锁 | 事务A锁1→2,事务B锁2→1 | 固定加锁顺序 |
| 间隙锁 | 范围查询后插入数据 | 降级RC或用唯一索引 |
| 不同索引加锁顺序 | 二级索引 vs 聚簇索引 | 统一通过主键操作 |
| 批量操作顺序不一致 | DELETE WHERE id IN(1,2)和(2,1) | 应用层ORDER BY id |
| 外键约束 | 子表插入检查父表 | 应用层维护关联 |
💡面试官想要的满分总结:
"MySQL死锁是并发事务交叉加锁导致的循环等待,InnoDB通过死锁检测自动回滚代价较小的事务。
常见死锁场景:
- 交叉加锁(最典型):事务A锁1→2,事务B锁2→1
- 间隙锁死锁:范围查询后插入数据,互相等待间隙锁释放
- 不同索引加锁顺序:二级索引和聚簇索引顺序不一致
定位方法:
SHOW ENGINE INNODB STATUS查看最近死锁日志information_schema.INNODB_TRX/LOCKS/LOCK_WAITS实时监控
解决方案:
- 固定加锁顺序(最重要)
- 使用RC隔离级别(无间隙锁)
- 缩小事务范围,减少锁持有时间
- 批量操作时
ORDER BY id统一顺序- 应用层
@Retryable重试机制
一句话:死锁无法完全避免,但可通过统一加锁顺序、缩小事务范围、使用RC隔离级别大幅降低概率,配合重试机制保证业务最终成功。"
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯