MySQL死锁问题全解析
骚话王又来分享知识了!今天咱们聊聊数据库界的"宫心计"——MySQL死锁问题。死锁这玩意儿,表面风平浪静,实则暗流涌动,稍不留神就让你的SQL卡壳,业务停摆。别慌,今天就带你拨开迷雾,直击死锁本质,顺便聊聊怎么优雅地解决它。
死锁是什么?
死锁(Deadlock)就是两个或多个事务在争夺资源时,互相等待对方释放锁,结果谁也等不到,大家都僵在那儿。就像两个人在窄巷子里互相谦让,结果谁也不让谁,最后都过不去。
死锁的本质与锁类型
在MySQL InnoDB存储引擎中,锁的类型五花八门,主要有:
- 行锁(Record Lock):只锁住某一行,精确打击,效率高。
- 间隙锁(Gap Lock):锁住一个区间,防止"插队",常见于范围查询。
- 临键锁(Next-Key Lock):行锁+间隙锁的组合拳,既锁住行又锁住区间。
- 表锁:一锁锁全表,简单粗暴,适合大扫除。
死锁的产生,往往和这些锁的组合、加锁顺序密不可分。
事务隔离级别对死锁的影响
MySQL支持四种事务隔离级别:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read,InnoDB默认)
- 串行化(Serializable)
隔离级别越高,锁的粒度和范围越大,死锁概率也随之上升。比如串行化级别下,所有读写都要加锁,死锁风险直线上升。
死锁的常见场景
1. 不同顺序加锁
假设有两张表A和B,两个事务分别如下:
- 事务1:先锁A,再锁B
- 事务2:先锁B,再锁A
如果两者几乎同时执行,事务1锁住A后等B,事务2锁住B后等A,互相等待,死锁就发生了。
举个栗子:
-- 事务1
BEGIN;
UPDATE A SET ... WHERE id=1;
UPDATE B SET ... WHERE id=1;
-- 事务2
BEGIN;
UPDATE B SET ... WHERE id=1;
UPDATE A SET ... WHERE id=1;
2. 范围锁(间隙锁、意向锁)引发的死锁
InnoDB的间隙锁、意向锁有时也会让人头大。比如两个事务都在插入或更新相邻区间的数据,锁的范围重叠,互相等待。
举个栗子:
-- 事务1
BEGIN;
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- 事务2
BEGIN;
UPDATE user SET age = age + 1 WHERE age = 25;
如果user表有大量age>20的数据,事务1锁住了大范围,事务2想更新age=25的数据时就会被卡住。
3. 外键约束导致的死锁
外键约束会让MySQL在更新/删除父表和子表时加锁顺序变复杂,容易出现死锁。
举个栗子:
-- 事务1
DELETE FROM parent WHERE id=1;
-- 事务2
DELETE FROM child WHERE parent_id=1;
如果两个事务同时进行,可能会因为外键约束加锁顺序不同而死锁。
4. 并发插入导致的死锁
高并发下,多个事务同时插入相同唯一索引的数据,也可能死锁。
举个栗子:
-- 事务1
INSERT INTO user(email) VALUES('a@a.com');
-- 事务2
INSERT INTO user(email) VALUES('a@a.com');
两个事务都想插入同一条唯一数据,InnoDB会加锁,最后一个会被回滚。
5. 业务逻辑复杂导致的死锁
比如订单系统,先扣库存再扣余额,另一个事务先扣余额再扣库存,顺序一变,死锁就来了。
举个栗子:
-- 事务1
BEGIN;
UPDATE stock SET count = count - 1 WHERE id=100;
UPDATE account SET balance = balance - 100 WHERE user_id=1;
-- 事务2
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id=1;
UPDATE stock SET count = count - 1 WHERE id=100;
6. 批量操作与死锁
批量更新、删除时,如果SQL没有排序,InnoDB会按主键顺序加锁,多个事务批量操作时主键顺序不一致,也容易死锁。
**骚话王提醒:**批量操作时加上ORDER BY,让大家排好队,减少死锁概率。
死锁是怎么产生的?
归根结底,死锁的本质是"资源争夺+互相等待"。常见诱因有:
- 多事务并发,锁定资源顺序不一致
- 范围锁、间隙锁导致锁粒度扩大
- 外键、唯一索引等隐式加锁
- 业务逻辑复杂,锁定范围不可控
- 批量操作未排序
- 事务未及时提交,锁长期占用
如何发现死锁?
MySQL会自动检测死锁,并回滚其中一个事务。你可以通过以下方式排查:
SHOW ENGINE INNODB STATUS; -- 查看最近一次死锁信息
日志里会有详细的死锁信息,包括涉及的SQL、锁类型、等待链路等。
骚话王实战技巧:
- 死锁日志只保留最近一次,建议死锁高发时定时采集日志。
- 关注
LATEST DETECTED DEADLOCK段落,里面有详细的锁等待链路。 - 配合
performance_schema.events_statements_history表,追踪死锁SQL来源。
MySQL参数对死锁的影响
innodb_lock_wait_timeout:锁等待超时时间,默认50秒。死锁发生时一般不会等这么久,MySQL会立刻检测并回滚。innodb_deadlock_detect:是否开启死锁检测,默认ON。关闭后死锁不会自动检测,容易出现"僵尸事务"。innodb_print_all_deadlocks:开启后,所有死锁信息都会记录到错误日志,方便排查。
死锁怎么解决?
骚话鬼才给你支几招:
1. 保持加锁顺序一致
所有事务按照相同顺序加锁,避免"你等我、我等你"的尴尬。
2. 减小锁定范围
能用行锁就别用表锁,能精确where就别全表扫描,减少锁冲突。
3. 合理设计索引和外键
索引覆盖、外键约束要慎用,避免隐式加锁带来的死锁风险。
4. 捕获并重试
应用层捕获死锁异常(如1213: Deadlock found),适当重试,保证业务可用性。
**骚话王建议:**重试次数不宜过多,防止"死锁风暴"。
5. 批量操作加排序
批量更新、删除时加上ORDER BY,让加锁顺序一致,减少死锁。
6. 及时提交事务
事务做完就提交,别让锁"霸占"资源,给别人留条活路。
7. 分析死锁日志,优化SQL
定期分析SHOW ENGINE INNODB STATUS,找出高发死锁SQL,优化逻辑。
8. 线上排查与优化建议
- 监控死锁频率,发现异常及时报警。
- 结合慢查询日志、业务日志,定位高危SQL。
- 适当拆分大事务,降低锁冲突概率。
- 业务高峰期可考虑降级处理,减少并发写入。
代码层面防止死锁的最佳实践
- 保证所有业务代码加锁顺序一致
- 批量操作加排序
- 事务尽量短小精悍,减少锁持有时间
- 捕获死锁异常,合理重试
- 代码review时关注SQL加锁顺序