1. 死锁现象与基础排查手段
第一次在监控系统里看到数据库连接数飙升时,我的手心直冒汗。那是个周五的下午,电商平台的订单提交接口突然大面积超时。打开终端连上数据库,输入SHOW ENGINE INNODB STATUS后,满屏的锁等待信息让我意识到——我们遇到了典型的MySQL死锁。
死锁的本质就像两个人在窄桥相遇:事务A握着资源X等待资源Y,事务B却握着资源Y在等资源X。MySQL检测到这种循环等待会自动触发死锁处理机制,但我们需要快速定位问题源头。以下是每个DBA都应该掌握的紧急排查命令:
-- 查看当前锁等待情况(MySQL 5.7+) SELECT * FROM performance_schema.events_waits_current WHERE EVENT_NAME LIKE '%lock%'; -- 经典的三张锁信息表(适用于所有版本) SELECT * FROM information_schema.INNODB_TRX; -- 运行中的事务 SELECT * FROM information_schema.INNODB_LOCKS; -- 持有的锁 SELECT * FROM information_schema.INNODB_LOCK_WAITS;-- 锁等待关系上周处理的一个案例特别典型:批量订单状态更新时,两个事务以相反顺序更新了相同的订单记录。通过INNODB_LOCK_WAITS表发现事务1234在等待事务5678释放行锁,而事务5678又在等待事务1234的另一把锁,典型的交叉更新死锁。
2. 死锁日志深度解析
拿到SHOW ENGINE INNODB STATUS输出的死锁日志时,别被那一大段十六进制吓倒。我们来看个真实案例的关键部分:
LATEST DETECTED DEADLOCK ------------------------ 2024-03-15 14:22:17 0x7f445c2b6700 *** (1) TRANSACTION: TRANSACTION 3025831, ACTIVE 12 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) UPDATE orders SET status='paid' WHERE order_id=1005 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 85 page no 4 n bits 72 index PRIMARY of table `db`.`orders` trx id 3025831 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 3025830, ACTIVE 15 sec updating or deleting mysql tables in use 1, locked 1 4 lock struct(s), heap size 1136, 3 row lock(s) UPDATE order_items SET shipped=1 WHERE order_id=1005 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 85 page no 4 n bits 72 index PRIMARY of table `db`.`orders` trx id 3025830 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 86 page no 3 n bits 80 index idx_order_id of table `db`.`order_items` trx id 3025830 lock_mode X locks rec but not gap waiting日志拆解指南:
- 事务时间线:事务3025831比3025830晚3秒启动
- 锁模式解读:
lock_mode X是排他锁,rec but not gap是记录锁(非间隙锁) - 关键冲突点:
- 事务3025830持有orders表主键锁(order_id=1005)
- 事务3025831在等待同一个主键锁
- 事务3025830又在等待order_items表的二级索引锁
这个死锁的根本原因是应用层先更新订单主表再更新条目表的模式,与另一个服务先更新条目表再更新主表的模式产生了资源竞争。
3. 六种典型死锁场景分析
3.1 交叉更新死锁
-- 事务A BEGIN; UPDATE products SET stock=stock-1 WHERE id=1; UPDATE products SET stock=stock-1 WHERE id=2; -- 事务B BEGIN; UPDATE products SET stock=stock-1 WHERE id=2; UPDATE products SET stock=stock-1 WHERE id=1;解决方案:统一更新顺序,比如按照id升序处理。
3.2 间隙锁冲突
在RR隔离级别下,这个场景经常让人措手不及:
-- 事务A SELECT * FROM orders WHERE amount > 100 FOR UPDATE; -- 事务B INSERT INTO orders(amount) VALUES (150); -- 被阻塞优化方案:如果业务允许,改用RC隔离级别避免间隙锁。
3.3 唯一键冲突
批量导入数据时最容易触发:
-- 事务A INSERT INTO users(email) VALUES ('a@test.com'); -- 事务B INSERT INTO users(email) VALUES ('a@test.com'); -- 等待 -- 事务A再插入其他记录就会死锁规避方法:使用INSERT IGNORE或ON DUPLICATE KEY UPDATE。
4. 系统化排查流程
建立标准排查流程能节省大量时间,这是我的检查清单:
紧急止血
-- 查找阻塞源头 SELECT blocking_pid, COUNT(*) FROM sys.innodb_lock_waits GROUP BY blocking_pid; -- 终止恶性进程 KILL 12345;日志收集
# 持续监控死锁日志 mysqladmin debug 2>&1 | grep -A 50 "LATEST DETECTED DEADLOCK"模式分析
- 统计高频死锁表:
grep -o "table.*" deadlock.log | sort | uniq -c - 绘制事务时序图:用工具可视化锁等待关系
- 统计高频死锁表:
复现验证
-- 在测试环境模拟 SET GLOBAL innodb_status_output_locks=ON; -- 执行可疑事务序列
5. 预防与优化策略
应用层优化:
- 所有事务按照固定顺序访问资源
- 缩短事务执行时间,避免用户交互
- 批量操作使用
LIMIT分片处理
数据库配置:
# my.cnf关键参数 innodb_lock_wait_timeout=30 # 默认50秒太长 innodb_deadlock_detect=ON # 8.0+默认开启 transaction_isolation=READ-COMMITTED架构设计:
- 热点数据采用队列串行处理
- 使用Redis缓存减轻数据库压力
- 考虑改用乐观锁机制
记得去年双十一大促前,我们通过分析历史死锁日志,发现90%的死锁集中在库存扣减场景。最终通过引入Redis分布式锁+本地事务的组合方案,将死锁发生率降为零。这提醒我们——治本之策永远是优化业务逻辑,而不是单纯调优数据库参数。