了解MySQL死锁问题

115 阅读6分钟

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加锁顺序