文章目录
- Pre
- 事务的隔离级别
- 怎么理解脏读、不可重复读和幻读?
- 怎么理解死锁
- 间隙锁与MVCC
-
- [间隙锁(Gap Lock)](#间隙锁(Gap Lock))
- MVCC(多版本并发控制)
- [间隙锁与 MVCC 的协同](#间隙锁与 MVCC 的协同)
- 应用场景与优化建议

Pre
MySQL索引原理与优化指南:深入解析B+Tree与高效查询策略
事务的隔离级别
MySQL 的事务隔离级别(Isolation Level) 是指:当多个线程操作数据库时,数据库要负责隔离操作,来保证各个线程在获取数据时的准确性。
它分为四个不同的层次,按隔离水平高低排序,读未提交 < 读已提交 < 可重复度 < 串行化。

MySQL提供四种事务隔离级别,按隔离强度递增排序:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 原理与实现机制 |
---|---|---|---|---|
读未提交 | 是 | 是 | 是 | 不加锁,直接读取最新数据(包括未提交的)。性能最高,但数据一致性最差。 |
读已提交 | 否 | 是 | 是 | 通过MVCC(多版本并发控制)实现,每次读取已提交的最新快照,解决脏读。 |
可重复读 | 否 | 否 | 否* | InnoDB默认级别,使用MVCC+间隙锁,确保事务内多次读取一致性,解决幻读。 |
串行化 | 否 | 否 | 否 | 所有操作加锁(共享读锁/排他写锁),完全串行化,性能最低但一致性最强。 |
*注:InnoDB在可重复读级别下通过间隙锁和MVCC有效防止幻读。
-
读未提交(Read uncommitted):隔离级别最低、隔离度最弱,脏读、不可重复读、幻读三种现象都可能发生。所以它基本是理论上的存在,实际项目中没有人用,但性能最高。
-
读已提交(Read committed):它保证了事务不出现中间状态的数据,所有数据都是已提交且更新的,解决了脏读的问题。但读已提交级别依旧很低,它允许事务间可并发修改数据,所以不保证再次读取时能得到同样的数据,也就是还会存在不可重复读、幻读的可能。
-
可重复读(Repeatable reads) :MySQL InnoDB 引擎的默认隔离级别,保证同一个事务中多次读取数据的一致性,解决脏读和不可重复读,但仍然存在幻读的可能。
-
可串行化(Serializable):选择"可串行化"意味着读取数据时,需要获取共享读锁;更新数据时,需要获取排他写锁;如果 SQL 使用 WHERE 语句,还会获取区间锁。换句话说,事务 A 操作数据库时,事务 B 只能排队等待,因此性能也最低。
至于数据库锁,分为悲观锁和乐观锁,"悲观锁"认为数据出现冲突的可能性很大,"乐观锁"认为数据出现冲突的可能性不大。那悲观锁和乐观锁在基于 MySQL 数据库的应用开发中,是如何实现的呢?
-
悲观锁一般利用
SELECT ... FOR UPDATE
类似的语句,对数据加锁,避免其他事务意外修改数据。 -
乐观锁利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,实现版本判断。
怎么理解脏读、不可重复读和幻读?
脏读
读到了未提交事务的数据

假设有 A 和 B 两个事务,在并发情况下,事务 A 先开始读取商品数据表中的数据,然后再执行更新操作,如果此时事务 A 还没有提交更新操作,但恰好事务 B 开始,然后也需要读取商品数据,此时事务 B 查询得到的是刚才事务 A 更新后的数据。
如果接下来事务 A 触发了回滚,那么事务 B 刚才读到的数据就是过时的数据,这种现象就是脏读。
-
脏读对应的隔离级别是"读未提交",只有该隔离级别才会出现脏读。
-
脏读的解决办法是升级事务隔离级别,比如"读已提交"。
不可重复读
事务 A 先读取一条数据,然后执行逻辑的过程中,事务 B 更新了这条数据,事务 A 再读取时,发现数据不匹配,这个现象就是"不可重复读"

-
简单理解是两次读取的数据中间被修改,对应的隔离级别是"读未提交"或"读已提交"。
-
不可重复读的解决办法就是升级事务隔离级别,比如"可重复度"。
幻读
在一个事务内,同一条查询语句在不同时间段执行,得到不同的结果集。

事务 A 读了一次商品表,得到最后的 ID 是 3,事务 B 也同样读了一次,得到最后 ID 也是 3。接下来事务 A 先插入了一行,然后读了一下最新的 ID 是 4,刚好是前面 ID 3 加上 1,然后事务 B 也插入了一行,接着读了一下最新的 ID 发现是 5,而不是 3 加 1。
这时,你发现在使用 ID 做判断或做关键数据时,就会出现问题,这种现象就像是让事务 B 产生了幻觉一样,读取到了一个意想不到的数据,所以叫幻读。当然,不仅仅是新增,删除、修改数据也会发生类似的情况。
-
要想解决幻读不能升级事务隔离级别到"可串行化",那样数据库也失去了并发处理能力。
-
行锁解决不了幻读,因为即使锁住所有记录,还是阻止不了插入新数据。
-
解决幻读的办法是锁住记录之间的"间隙",为此 MySQL InnoDB 引入了新的锁,叫间隙锁(Gap Lock),所以在面试中,你也要掌握间隙锁,以及间隙锁与行锁结合的 next-key lock 锁。
怎么理解死锁
死锁是如何产生的
死锁一般发生在多线程(两个或两个以上)执行的过程中。因为争夺资源造成线程之间相互等待,这种情况就产生了死锁。

比如有资源 1 和 2,以及线程 A 和 B,当线程 A 在已经获取到资源 1 的情况下,期望获取线程 B 持有的资源 2。与此同时,线程 B 在已经获取到资源 2 的情况下,期望获取现场 A 持有的资源 1。
那么线程 A 和线程 B 就处理了相互等待的死锁状态,在没有外力干预的情况下,线程 A 和线程 B 就会一直处于相互等待的状态,从而不能处理其他的请求。
死锁产生的四个必要条件
互斥

互斥: 多个线程不能同时使用一个资源。比如线程 A 已经持有的资源,不能再同时被线程 B 持有。如果线程 B 请求获取线程 A 已经占有的资源,那线程 B 只能等待这个资源被线程 A 释放。
持有并等待

持有并等待: 当线程 A 已经持有了资源 1,又提出申请资源 2,但是资源 2 已经被线程 C 占用,所以线程 A 就会处于等待状态,但它在等待资源 2 的同时并不会释放自己已经获取的资源 1。
不可剥夺

不可剥夺: 线程 A 获取到资源 1 之后,在自己使用完之前不能被其他线程(比如线程 B)抢占使用。如果线程 B 也想使用资源 1,只能在线程 A 使用完后,主动释放后再获取。
循环等待

循环等待: 发生死锁时,必然会存在一个线程,也就是资源的环形链。比如线程 A 已经获取了资源 1,但同时又请求获取资源 2。线程 B 已经获取了资源 2,但同时又请求获取资源 1,这就会形成一个线程和资源请求等待的环形图。
如何规避死锁
死锁只有同时满足互斥、持有并等待、不可剥夺、循环等待时才会发生。并发场景下一旦死锁,一般没有特别好的方法,很多时候只能重启应用。因此,最好是规避死锁,那么具体怎么做呢?答案是:至少破坏其中一个条件(互斥必须满足,你可以从其他三个条件出发)。
-
持有并等待:我们可以一次性申请所有的资源,这样就不存在等待了。
-
不可剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。
-
循环等待:可以靠按序申请资源来预防,也就是所谓的资源有序分配原则,让资源的申请和使用有线性顺序,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样的线性化操作就自然就不存在循环了。
间隙锁与MVCC
间隙锁(Gap Lock)
1. 间隙锁的核心作用
InnoDB 引擎在可重复读(Repeatable Read)隔离级别下,通过间隙锁解决幻读问题。
- 锁定范围 :间隙锁不锁定记录本身,而是锁定索引记录的间隙区间 (如
id > 10
的范围)。 - 防止插入:阻止其他事务在间隙中插入新数据,从而保证事务范围内数据的一致性。
2. 间隙锁的工作机制
- 触发条件 :当执行
SELECT ... FOR UPDATE
或UPDATE/DELETE
等当前读操作时,InnoDB 自动对查询涉及的索引范围加锁。 - 锁类型 :
- 记录锁(Record Lock):锁定索引中的具体记录。
- 间隙锁(Gap Lock) :锁定记录之间的间隙(如
(5, 10)
区间)。 - 临键锁(Next-Key Lock) :记录锁 + 间隙锁的组合(如
(5, 10]
),锁定左开右闭区间。
3. 示例场景
sql
-- 事务 A
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE;
-- 对 age 索引中所有大于 20 的间隙加锁
-- 事务 B
INSERT INTO users (name, age) VALUES ('John', 25); -- 被阻塞(间隙锁生效)
4. 间隙锁的特性
- 范围锁定 :仅对非唯一索引 或范围查询生效。
- 冲突场景:间隙锁之间可以共存(允许不同事务锁定不同间隙),但间隙锁与插入意向锁冲突。
- 性能影响:过度使用间隙锁可能导致锁竞争和死锁(如两个事务互相等待对方释放间隙)。
5. 间隙锁的局限性
- 仅对写操作有效 :读操作(如普通
SELECT
)默认使用 MVCC,不受间隙锁影响。 - 仅在可重复读级别生效:在读已提交(Read Committed)级别,InnoDB 会释放间隙锁。
MVCC(多版本并发控制)
1. MVCC 的核心思想
通过维护数据的多个版本,实现非锁定一致性读,解决读写冲突,提高并发性能。
- 快照读(Snapshot Read) :读取事务开始时的数据快照(通过
UNDO 日志
构建)。 - 当前读(Current Read) :读取最新数据并加锁(如
SELECT ... FOR UPDATE
)。
2. MVCC 的实现机制
(1)隐藏字段
InnoDB 为每行数据隐式添加 3 个字段:
DB_TRX_ID
:最后修改该行的事务 ID。DB_ROLL_PTR
:指向旧版本数据的指针(通过UNDO 日志
构建版本链)。DB_ROW_ID
:隐含的行 ID(无主键时自动生成聚簇索引)。
(2)ReadView 的生成规则
- 读已提交(RC):每次查询生成新的 ReadView,读取已提交的最新快照。
- 可重复读(RR):事务开始时生成一次 ReadView,后续读操作复用该视图。
(3)版本可见性判断
InnoDB 根据事务的 DB_TRX_ID
和 ReadView 判断数据版本是否可见:
- 已提交事务 :若
DB_TRX_ID < ReadView 中最小事务ID
,数据可见。 - 未提交事务 :若
DB_TRX_ID
在活跃事务列表中,数据不可见。 - 后续事务 :若
DB_TRX_ID > ReadView 中最大事务ID
,数据不可见。
3. MVCC 的示例场景
sql
-- 事务 A(事务ID=100)
BEGIN;
SELECT * FROM products WHERE id=1; -- 读取快照版本(假设此时 version=1)
-- 事务 B(事务ID=101)更新同一行
UPDATE products SET stock=5 WHERE id=1; -- 生成新版本(version=2)
-- 事务 A 再次读取
SELECT * FROM products WHERE id=1; -- 仍读取 version=1(可重复读特性)
4. MVCC 的优势与限制
- 优势 :
- 读写不冲突,提高并发性能。
- 保证事务内的数据一致性(可重复读)。
- 限制 :
- 需要维护
UNDO 日志
,占用额外存储空间。 - 长期未提交的事务可能导致
UNDO 日志
无法清理(长事务问题)。
- 需要维护
间隙锁与 MVCC 的协同
1. 解决幻读的完整方案
- MVCC :通过快照读避免非锁定读 的幻读(如普通
SELECT
)。 - 间隙锁 :通过锁定索引范围阻止插入操作 ,解决锁定读 (如
SELECT ... FOR UPDATE
)的幻读。
2. 示例:可重复读下的幻读防御
sql
-- 事务 A(可重复读级别)
BEGIN;
SELECT * FROM users WHERE age > 20; -- 快照读(无锁,使用 MVCC)
-- 此时事务 B 插入 age=25 的新记录(未被阻塞)
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 当前读,加间隙锁
-- 事务 B 的插入操作被阻塞(间隙锁生效)
3. 关键结论
- InnoDB 的 可重复读(RR) 级别通过 间隙锁 + MVCC 实现真正的幻读防御(优于 SQL 标准)。
- MVCC 解决非锁定读 的幻读,间隙锁解决锁定读的幻读。
应用场景与优化建议
1. 间隙锁的优化场景
- 范围更新 :批量更新或删除时,显式缩小锁定范围(如
WHERE id BETWEEN 1 AND 100
)。 - 唯一索引优先:尽量使用唯一索引,减少间隙锁的使用(唯一索引查询退化为记录锁)。
2. MVCC 的优化建议
- 避免长事务 :及时提交事务,减少
UNDO 日志
的存储压力。 - 合理选择隔离级别:读已提交(RC)级别下 MVCC 更轻量(每次查询生成新快照)。
3. 死锁预防
- 按序访问资源:所有事务按相同顺序加锁(如先更新表 A 再更新表 B)。
- 减少锁定范围 :使用精确条件(如
WHERE id=1
而非WHERE id>1
)。
通过理解间隙锁与 MVCC 的协同机制,可以更好地设计高并发场景下的数据库操作,平衡一致性与性能。
