theme: default themeName: "默认主题" title: "MySQL死锁把应用卡死?一张图教你读懂死锁日志,根因定位只需5分钟"
前言
生产环境最怕两种数据库问题:一是慢查询拖垮性能,二是死锁导致业务直接报错。尤其死锁,它不像慢查询那样给你缓冲时间——一旦触发,事务直接回滚,业务接口瞬间返给你一个"Deadlock found"错误。多数运维看到死锁日志就头皮发麻,其实只要掌握正确的分析方法,定位根因也就是一泡尿的功夫。今天就把我的排查套路完整抖出来,看完你也能独立分析死锁。
死锁是怎么形成的
先铺垫几个基本概念,防止后面看日志时懵圈。
事务:一组原子性SQL操作,要么全部成功,要么全部回滚。MySQL默认autocommit=1,每条SQL自动提交一个事务。 行锁:InnoDB引擎的行级锁,锁定的是表中的具体行数据。锁的是索引记录,不是整行数据。 GAP锁:间隙锁,锁定的是索引记录之间的空隙,防止其他事务插入新记录。 死锁形成条件:1. 互斥:资源只能被一个事务占用 2. 占有并等待:事务已经持有资源,还想申请其他资源 3. 不可抢占:资源不能被强制抢走,只能主动释放 4. 循环等待:形成事务之间的等待环
说人话:事务A锁住了记录1,等着拿记录2;事务B锁住了记录2,等着拿记录1。互相等对方放手,就死锁了。
死锁日志怎么读
MySQL会自动检测死锁并回滚其中一个事务,同时在错误日志中记录详细的死锁信息。来看一个典型的死锁日志:
* (1) TRANSACTION:
TRANSACTION 12345678, ACTIVE 10 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 999, OS thread handle 12345, query id 888 localhost root update INSERT INTO order_detail (order_id, product_id, quantity) VALUES (1001, 2001, 5)
* (2) TRANSACTION:
TRANSACTION 12345679, ACTIVE 8 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 1000, OS thread handle 12346, query id 889 localhost root update INSERT INTO order_detail (order_id, product_id, quantity) VALUES (1002, 2002, 3)
* (2) TRANSACTION 12345679 HOLDS THE LOCK(S) RECORD LOCKS space id 88 page no 3 n bits 72 index PRIMARY of table `shop`.`order_detail` trx id 12345679 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 00002711; asc '2001';; 1: len 6; hex 0000000002c1; asc '2001';;
* (1) TRANSACTION 12345678 WAITING FOR THIS LOCK TO BE GRANTED
RECORD LOCKS space id 88 page no 3 n bits 72 index PRIMARY of table `shop`.`order_detail` trx id 12345678 lock_mode X locks rec but not gap waiting
这段日志信息量很大,我们来拆解:
TRANSACTION 1 正在执行INSERT,持有锁并等待另一个锁 TRANSACTION 2 正在执行INSERT,持有锁X,阻塞了事务1看最后的`lock_mode X locks rec but not gap waiting`:X是排他锁,`not gap`说明是记录锁(行锁),`waiting`表示正在等待这个锁。
核心矛盾:两个事务都在插入记录,但插入的记录之间形成了循环等待。
常见死锁模式
模式一:主键索引死锁
两个事务以相反顺序更新主键:
-- 事务1
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; -- 先锁id=1 UPDATE account SET balance = balance + 100 WHERE id = 2; -- 等锁id=2
-- 事务2(同时执行) START TRANSACTION; UPDATE account SET balance = balance - 200 WHERE id = 2; -- 先锁id=2 UPDATE account SET balance = balance + 200 WHERE id = 1; -- 等锁id=1
解决:统一SQL执行顺序,按主键顺序更新
模式二:非主键索引死锁
-- 事务1
UPDATE user SET status = 1 WHERE phone = '13800138000'; -- 锁索引A DELETE FROM user WHERE id = 10; -- 锁主键
-- 事务2 DELETE FROM user WHERE id = 10; -- 锁主键 UPDATE user SET status = 1 WHERE phone = '13800138000'; -- 锁索引A
两个事务操作的是不同索引,但最终都要访问对方锁住的主键,形成循环等待。
模式三:GAP锁导致的死锁
-- 事务1
INSERT INTO order_detail (order_id, product_id) VALUES (100, 200);
-- 事务2(同时插入相同记录) INSERT INTO order_detail (order_id, product_id) VALUES (100, 200);
两个事务同时插入相同唯一键值,后执行的事务会等待前一个事务释放GAP锁,如果前事务回滚,后事务获取锁继续插入成功——这本身不是死锁,但如果配合其他操作就可能形成死锁。
如何主动排查死锁
1. 开启死锁详细信息日志
# my.cnf配置
innodb_print_all_deadlocks = 1 innodb_status_output = ON innodb_status_output_locks = ON
2. 实时查看当前锁状态
-- 查看当前正在等待的锁
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看当前持有的锁 SELECT * FROM information_schema.INNODB_LOCKS;
-- 查看事务详情 SELECT * FROM information_schema.INNODB_TRX;
3. 开启慢查询日志捕获问题SQL
# my.cnf
slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1
死锁往往发生在高并发+复杂SQL场景,把慢SQL优化了,死锁概率自然下降。
一个真实案例:订单并发扣款死锁
某电商平台做促销活动时,大量用户反馈支付失败,查看错误日志全是"Deadlock found"。
问题分析:- 业务逻辑:用户下单 → 扣减库存 → 创建订单 → 支付
- 并发场景:同一商品多人同时购买
- 死锁位置:库存表`product_stock`的UPDATE语句
-- 订单A:先扣product_id=1,再扣product_id=2
UPDATE product_stock SET stock = stock - 1 WHERE product_id = 1; UPDATE product_stock SET stock = stock - 1 WHERE product_id = 2;
-- 订单B:先扣product_id=2,再扣product_id=1 UPDATE product_stock SET stock = stock - 1 WHERE product_id = 2; UPDATE product_stock SET stock = stock - 1 WHERE product_id = 1;
解决方案:
1. 应用层统一扣减顺序:按product_id排序后再执行UPDATE 2. 或者用SELECT FOR UPDATE显式加锁,强制按顺序获取锁
预防死锁的黄金法则
1. 统一操作顺序:所有事务按相同顺序访问资源 2. 缩小锁范围:只锁必要的行,别用SELECT *全表扫描触发间隙锁 3. 降低事务复杂度:事务内SQL越少越好,批量操作考虑拆分 4. 使用低隔离级别:READ COMMITTED比REPEATABLE READ少用GAP锁 5. 加超时机制:业务代码设置锁等待超时,别无限等下去
# Python示例:设置事务超时
cursor.execute("SET innodb_lock_wait_timeout = 10")
结语
死锁不可怕,可怕的是不会看日志。MySQL的死锁日志已经把所有信息都暴露给你了——哪个事务在等哪把锁、锁的是哪个索引、具体是哪条SQL。顺着日志往下挖,80%的死锁都是操作顺序问题,改改SQL写法就能解决。下次遇到Deadlock报错,直接把日志贴过来按着我教的步骤分析,五分钟定位不是梦。
看完还有什么疑问吗?
如果文章没有覆盖到你的情况,欢迎联系我们咨询——免费解答,说清楚再决定要不要服务。
📞 服务热线:13708730161 💬 微信:eyc1689 📧 邮箱:service@eycit.com 🌐 https://www.eycit.com
易云城IT服务,您身边的IT专家。