服务覆盖:昆明·曲靖·玉溪·保山·昭通·丽江·普洱·临沧·楚雄·红河·文山·西双版纳·大理·德宏·怒江·迪庆

MySQL死锁自救指南:我在生产环境踩过的那些坑

eycit 2026-04-21 -1 次阅读 系统安装
---

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专家。

上一篇
服务器又双叒叕挂了?手把手教你三招定位「隐形杀手」...
下一篇
网站访问慢?先别急着加配置,可能是DNS在搞鬼...