HelloCoder HelloCoder
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
  • 《LearnJavaToFindAJob》

    • 导读

    • 【初级】6~12k档

    • 【中级】12k-26k档

      • JVM进阶

      • Java进阶

      • MySQL

        • Innodb和MyISAM索引的区别
        • MySQL-like是否可以使用索引
        • MySQL一些索引失效的场景和原理
        • MySQL大表索引重建
        • MySQL如何行转列?
        • MySQL死锁的场景
        • MySQL的架构和执行流程
        • MySQL的缓存
        • MySQL的自增ID用完了会怎样?
        • MySQL索引的分类、何时使用、何时不使用、何时失效?
        • MySQL联合索引在B+数的存储结构和最左匹配原则原理
        • MySQL连表优化
        • MySQL连表查询的原理
        • MySQL那种事务隔离级别性能高
        • Mysql的索引和主键的区别
        • binlog、redolog、undolog的区别和作用
        • 什么是前缀索引,什么情况才使用?
        • 可重复读是否能解决幻读?
        • 我以为我对Mysql事务很熟,直到我遇到了阿里面试官
        • 聊聊MySQL索引的分类和结构吧
        • 脏读、幻读
      • 中间件

      • 算法

      • 高阶

    • 【高级】26k+档

    • 大厂面试题

    • 求职建议

    • 面经

  • LearnJavaToFindAJob
  • 【中级】12k-26k档
  • MySQL
#MySQL #死锁的场景
HaC
2026-06-25
目录

MySQL死锁的场景

在 MySQL(尤其是使用 InnoDB 存储引擎)中,死锁(Deadlock) 是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。

MySQL 底层有一套死锁检测机制(wait-for-graph 算法),一旦发现死锁,会选择回滚代价最小的那个事务,让另一个事务成功执行。

虽然 MySQL 会自动处理,但死锁会导致业务请求报错、吞吐量下降。以下是生产环境中最经典的 4 种死锁场景及底层原理拆解。

前提要先知道 共享锁S、排他锁 X (读读兼容,读写排斥,写写排斥),因为这些场景最终的原理都是这两个锁。

共享锁 (S 锁 - 读锁) 排他锁 (X 锁 - 写锁)
共享锁 (S 锁) 🟢 兼容 (允许) ❌ 冲突 (阻塞)
排他锁 (X 锁) ❌ 冲突 (阻塞) ❌ 冲突 (阻塞)

# 场景一:相反顺序加锁(最经典的死锁)

这是最常见、也最容易理解的死锁场景。两个事务由于代码逻辑问题,以交叉相反的顺序去更新两条记录。

# 1. 场景重现:

假设有用户 A(id=1)和用户 B(id=2),他们同时向对方转账。

时间点 (T) 事务 A (SQL) 事务 B (SQL) 底层锁状态与死锁成因
T1 BEGIN; BEGIN; 两个事务各自开启。
T2 UPDATE account SET balance=800 WHERE id=1; 加锁:事务 A 成功获取了 id=1 的排他锁 (X锁)。
T3 UPDATE account SET balance=700 WHERE id=2; 加锁:事务 B 成功获取了 id=2 的排他锁 (X锁)。
T4 UPDATE account SET balance=600 WHERE id=2; 冲突点 1:事务 A 尝试修改 id=2,但被事务 B 占有的 X 锁堵住,事务 A 进入挂起等待状态。
T5 UPDATE account SET balance=500 WHERE id=1; 冲突点 2:事务 B 尝试修改 id=1,被事务 A 占有的 X 锁堵住。至此双方互等,MySQL 立刻检测到死锁,选择回滚其中一个事务。

# 场景二:锁升级死锁(S 锁尝试升级为 X 锁)

原理:两个事务为了防止别人改数据,先各自获取了同一条记录的共享锁(S锁,读锁)。随后,两个事务又同时尝试通过 UPDATE 将其升级为排他锁(X锁,写锁)。

时间点 (T) 事务 A (SQL) 事务 B (SQL) 底层锁状态与死锁成因
T1 BEGIN; BEGIN; 两个事务各自开启。
T2 SELECT balance FROM account WHERE id=1 LOCK IN SHARE MODE; 加锁:事务 A 获取了 id=1 的共享锁 (S锁)。
T3 SELECT balance FROM account WHERE id=1 LOCK IN SHARE MODE; 加锁:事务 B 也成功获取了 id=1 的共享锁 (S锁)(因为 S 锁与 S 锁是相互兼容的,所以不阻塞)。
T4 UPDATE account SET balance=800 WHERE id=1; 冲突点 1:事务 A 想要修改数据,必须把 S 锁升级为 X锁。但 X 锁要求别人不能有锁,而事务 B 正拿着 S 锁不放,事务 A 被迫排队。
T5 UPDATE account SET balance=700 WHERE id=1; 冲突点 2:事务 B 此时也执行了修改,同样想要升级为 X 锁,必须等事务 A 释放 S 锁。双方都在等对方放开读锁,触发死锁。

如果 事务T3 - 事务B 加的是X锁,那么它就只能阻塞,等待事务A释放,此时 T4-事务A 执行更新结束,事务B就能获取到锁了。

# 场景三:间隙锁与插入意向锁冲突(并发插入死锁)

原理:仅在 Repeatable Read(可重复读)隔离级别下,两个事务查询了同一个不存在的记录,各自获得了间隙锁(Gap Lock)。随后它们又同时在该区间内进行 INSERT,插入动作引发的插入意向锁被对方的间隙锁死死堵住。

(注:假设当前表里只有 id=1 和 id=10 的数据,区间 (1, 10) 是空虚的)

时间点 (T) 事务 A (SQL) 事务 B (SQL) 底层锁状态与死锁成因
T1 BEGIN; BEGIN; 两个事务各自开启。
T2 SELECT * FROM account WHERE id=5 FOR UPDATE; 加锁:因为 id=5 不存在,InnoDB 在区间 (1, 10) 上加了 间隙锁 (Gap Lock),防止别人插入引发幻读。
T3 SELECT * FROM account WHERE id=6 FOR UPDATE; 加锁:事务 B 同样加了区间 (1, 10) 的 间隙锁 (Gap Lock)。(注意:间隙锁之间是允许并存的,不冲突)
T4 INSERT INTO account(id, balance) VALUES(3, 300); 冲突点 1:事务 A 尝试插入。插入动作需要获取 (1,10) 的插入意向锁。但插入意向锁被事务 B 的间隙锁排斥,事务 A 陷入排队。
T5 INSERT INTO account(id, balance) VALUES(4, 400); 冲突点 2:事务 B 尝试插入,其插入意向锁被事务 A 的间隙锁排斥。互相卡死在路口,触发死锁。

# 场景四:唯一索引冲突回滚(Unique Key 导致的死锁)

原理:这是生产环境中最诡异的死锁。当三个事务同时并发插入一条相同唯一键(如 name 唯一)的记录时,由于第一个事务的回滚,会导致后两个事务同时将持有的 S 锁尝试升级为 X 锁。

(注:假设 name 字段是唯一索引,目前表中无 Jack)

时间点 (T) 事务 A (SQL) 事务 B (SQL) 事务 C (SQL) 底层锁状态与死锁成因
T1 BEGIN; INSERT INTO users(name) VALUES('Jack'); 事务 A 插入成功。此时事务 A 持有了 Jack 这一行的 排他锁 (X锁)。
T2 BEGIN; INSERT INTO users(name) VALUES('Jack'); 事务 B 发现唯一键冲突。因为事务 A 还没提交,事务 B 只能原地排队,并转而申请 共享锁 (S锁)。
T3 BEGIN; INSERT INTO users(name) VALUES('Jack'); 事务 C 同样发现冲突,开始排队申请 共享锁 (S锁)。
T4 ROLLBACK; 关键转折点:事务 A 回滚,隐式释放了它的 X 锁。此时,事务 B 和事务 C 同时成功获取了该行的 S 锁。
T5 瞬间醒来,尝试完成插入 瞬间醒来,尝试完成插入 事务 B 和 C 抢到读锁后,必须把 S 锁升级为 X锁 才能完成 INSERT。因为对方也拿着 S 锁,升级动作在 T5 瞬间双双卡死,直接爆发死锁。

INSERT/UPDATE/DELETE 默认加X锁,默认读select 不加锁所以不阻塞,因为是快照读

# 💡 终极复盘与避坑圣经

把这四个表格对比着看,你会发现两个非常有意思的数据库定律:

  1. 间隙锁(Gap Lock)是万恶之源:场景三之所以会发生,是因为 MySQL 的 RR 隔离级别为了防止幻读而引入了间隙锁。如果你把隔离级别降到 Read Committed (RC),间隙锁不复存在,场景三的死锁也就自动消失了。
  2. 写操作必须走索引:如果你的 UPDATE 语句由于没有建索引而触发了全表扫描,那么 MySQL 会把整张表的所有记录和所有间隙全部加上锁。此时,只要有任何第二个并发事务在做写操作,几乎 100% 触发死锁灾难。
#MySQL#死锁的场景
上次更新: 2026-06-25 17:18:05
最近更新
01
MySQL支持的锁有哪些
06-25
02
HTTP 是不保存状态的协议, 如何保存用户状态
06-25
03
WebSocket、短轮询、长轮询的区别
06-25
更多文章>
Theme by Vdoing | Copyright © 2020-2026 HaC
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式