theme: default themeName: "默认主题" title: "MySQL死锁自救指南:我在生产环境踩过的那些坑"
MySQL死锁自救指南:我在生产环境踩过的那些坑
做运维的,谁没遇到过几次死锁?那种「数据库连接数飙升、业务接口集体超时、报警短信响个不停」的感觉,真是谁遇到谁懂。我第一次遇到死锁的时候,盯着`SHOW ENGINE INNODB STATUS`输出看了半小时,完全看不懂在讲啥——什么「lock rec but not gap」「next-key lock」,一个个术语看得我头皮发麻。
后来踩的坑多了,总算摸出点门道。今天把我的血泪经验整理出来,手把手教你如何排查和避免MySQL死锁。
死锁是怎么形成的?
先说点理论,不然直接上命令你也看不懂。
死锁的本质:两个或多个事务互相持有对方需要的锁,形成了循环等待。举个例子:
事务A:
UPDATE users SET balance = balance - 100 WHERE id = 1; -- 先锁住id=1 UPDATE users SET balance = balance + 100 WHERE id = 2; -- 试图锁id=2
事务B: UPDATE users SET balance = balance + 200 WHERE id = 2; -- 先锁住id=2 UPDATE users SET balance = balance - 200 WHERE id = 1; -- 试图锁id=1
事务A锁了1等2,事务B锁了2等1,完美闭环,死锁诞生。
InnoDB的锁类型:- Record Lock:锁住索引记录
- Gap Lock:锁住索引之间的间隙,防止幻读
- Next-Key Lock:Record Lock + Gap Lock的组合
- 意向锁:表级锁,表示「我将要锁某行」
记住一点:InnoDB的行锁是锁索引的,不是锁整行。如果你 WHERE 条件没走索引恭喜你,整表都可能被锁。
实战排查:死锁现场怎么分析?
第一步:开启死锁日志
默认MySQL可能不记录死锁详情,先检查:
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
如果`OFF`,设为`ON`:
SET GLOBAL innodb_print_all_deadlocks = ON;
然后在MySQL错误日志里就能看到死锁的详细信息了。
第二步:查看当前死锁
SHOW ENGINE INNODB STATUS;
这个命令会输出最近一次死锁的信息,包含:
- TRANSACTION:事务详情,包括事务ID、执行的SQL、持有锁
- WAITING FOR THIS LOCK TO BE GRANTED:在等什么锁
- HOLDS THE LOCK(S):持有哪些锁
- ROLLBACK TRANSACTION :哪个事务被回滚了
重点看HOLDS THE LOCK(S)和WAITING FOR部分,分析锁的竞争关系。
第三步:分析具体SQL
拿到死锁日志后,定位到具体SQL,问自己几个问题:
1. 是不是没走索引? 用`EXPLAIN`检查执行计划 2. 是不是批量更新顺序不一致? 多个事务对同样的记录更新,顺序要一致 3. 是不是长事务拖累了? 事务越长,持有锁的时间越久,冲突概率越高
我见过最离谱的一个case:某业务批量给用户发优惠券,1000条记录分10批更新,结果每批更新顺序不一样——第一批从ID 1开始,第二批从ID 500开始……这不是找死吗?后来改成按主键排序后统一更新,世界瞬间清净了。
如何从根上避免死锁?
1. 统一访问顺序
这是最重要的原则:所有事务对同一批资源的访问顺序必须一致。
比如都要修改用户A和用户B,甭管谁先谁后,大家都按ID从小到大来:
-- 事务A
UPDATE users SET ... WHERE id IN (1, 2) ORDER BY id; -- 事务B 也必须先改ID=1,再改ID=2
2. 尽量用主键或唯一索引
主键查询一定走索引,行锁锁的是主键索引,范围可控。
-- 好:主键精确查询
SELECT * FROM orders WHERE id = 12345;
-- 差:非索引字段范围查询 SELECT * FROM orders WHERE status = 'pending'; -- 可能锁住几十万行
3. 减小事务体积
别把所有业务逻辑都塞进一个事务。能分开做的东西分开做:
-- 不好:长事务
BEGIN; -- 查数据 -- 算来算去 -- 改数据 -- 等等等 COMMIT;
-- 好:短事务 BEGIN; UPDATE ... WHERE id = 1; COMMIT;
BEGIN; UPDATE ... WHERE id = 2; COMMIT;
4. 合理设置隔离级别
如果业务允许,读提交(READ COMMITTED)比可重复读(REPEATABLE READ)死锁概率低很多。InnoDB默认是RR,因为有MVCC和next-key lock防幻读,但如果你的业务不需要防幻读,完全可以降级:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
5. 加锁超时设置
别让死锁事务无限等下去,设置合理的锁等待超时:
-- 设置锁等待超时为10秒
SET GLOBAL innodb_lock_wait_timeout = 10;
超时后事务会回滚,虽然会影响一笔请求,但比死锁导致整个库不可用强。
6. 业务层加分布式锁
对于关键业务,可以在应用层加分布式锁(Redis/ZooKeeper),同一笔业务只允许一个实例处理,从根本上避免并发冲突。
一个真实案例
2024年双十一,我们遇到过这样的死锁:
-- 事务A:扣库存
UPDATE product_stock SET stock = stock - 1 WHERE product_id = 100 AND stock > 0;
-- 事务B:同样扣库存 UPDATE product_stock SET stock = stock - 1 WHERE product_id = 100 AND stock > 0;
两条完全一样的SQL,同时执行,死了。
根因分析:`stock > 0`这个条件虽然有索引,但两个事务都先判断了`stock > 0`,然后同时去扣——都觉得自己能扣成功。 解决方案:1. 用`SELECT ... FOR UPDATE`先锁住记录,再检查和更新 2. 或者在SQL里直接用`UPDATE ... WHERE stock = stock - 1 AND stock > 0`,利用数据库的原子性 3. 加上重试机制,死锁时业务层自动重试
工具推荐
- MySQL Enterprise Monitor:有专门的死锁分析界面
- Percona Toolkit:`pt-deadlock-logger`可以持续记录死锁到表
- GreatSQL:国内开源分支,有更友好的死锁分析工具
写在最后
死锁是分布式系统和数据库的「癌症」,没法100%避免,但可以做到:
1. 设计层面:统一访问顺序、减小事务体积 2. 运维层面:开启死锁日志、设置超时、监控告警 3. 应急层面:有完整的回滚和恢复预案
遇到死锁不要慌,先`SHOW ENGINE INNODB STATUS`,顺着日志分析SQL的执行顺序,大部分问题都能找到根因。
推荐阅读:
- 《高性能MySQL》 - 锁机制和事务隔离级别详解
- 《MySQL技术内幕:InnoDB存储引擎》 - 深入理解InnoDB锁
【放心,我们兜底】
不管你是自己尝试修复,还是需要专业人员上门,易云城IT服务都给你托底。修不好不收费,修好了质保期内随时找我。
📞 服务热线:13708730161 💬 微信:eyc1689 📧 邮箱:service@eycit.com 🌐 https://www.eycit.com
您身边的IT专家。