第4篇-事务隔离级别的实现原理
约 1512 字大约 5 分钟
2026-05-24
四、MySQL 是如何实现隔离级别的?
MySQL 的 InnoDB 存储引擎,使用 MVCC(多版本并发控制) 和 Next-Key Lock(临键锁) 来实现隔离级别。
4.1 READ UNCOMMITTED(读未提交)
实现原理:
- 读操作:不加锁,直接读最新的数据(包括未提交的)
- 写操作:加行锁(X 锁)
问题:
- 脏读:读到别人还没提交的数据
示例:
-- 事务 A
START TRANSACTION;
UPDATE users SET age = 20 WHERE id = 1; -- 更新但未提交
-- 事务 B(READ UNCOMMITTED)
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT age FROM users WHERE id = 1; -- 读到 20(脏读!)4.2 READ COMMITTED(读已提交)
实现原理:
- 读操作:不加锁,使用 MVCC,读已提交的最新快照
- 写操作:加行锁(X 锁)
解决的问题:
- 脏读:读到的数据都是已提交的
问题:
- 不可重复读:同一事务中,两次读取结果可能不同
示例:
-- 事务 A
START TRANSACTION;
UPDATE users SET age = 20 WHERE id = 1;
COMMIT; -- 提交
-- 事务 B(READ COMMITTED)
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT age FROM users WHERE id = 1; -- 读到 10(事务 A 还未提交)
-- 事务 A 提交后
SELECT age FROM users WHERE id = 1; -- 读到 20(不可重复读!)4.3 REPEATABLE READ(可重复读,MySQL 默认)
实现原理:
- 读操作:不加锁,使用 MVCC,读事务开始时的快照
- 写操作:加行锁(X 锁)+ Next-Key Lock(防止幻读)
解决的问题:
- 脏读
- 不可重复读:同一事务中,两次读取结果相同(因为读同一个快照)
问题:
- 幻读:同一事务中,两次查询返回的行数可能不同(但 MySQL 的 Next-Key Lock 在很大程度上避免了幻读)
示例:
-- 事务 A
START TRANSACTION;
SELECT age FROM users WHERE id = 1; -- 读到 10
UPDATE users SET age = 20 WHERE id = 1;
COMMIT;
-- 事务 B(REPEATABLE READ)
START TRANSACTION;
SELECT age FROM users WHERE id = 1; -- 读到 10(可重复读!)
-- 即使事务 A 提交了,这里还是读到 10(因为读的是快照)4.4 SERIALIZABLE(串行化)
实现:
- 读操作:加共享锁(S 锁)
- 写操作:加排他锁(X 锁)
- 所有读操作都加锁,相当于串行执行
解决的问题:
- 脏读
- 不可重复读
- 幻读
问题:
- 性能最差!
五、MVCC(多版本并发控制)的原理
MVCC 是 MySQL InnoDB 实现 READ COMMITTED 和 REPEATABLE READ 的核心机制。
5.1 什么是 MVCC?
MVCC 的核心思想是:为每行数据保存多个版本(快照),读操作读快照,写操作写新版本。
数据行:
+----+-----+----------+----------+
| id | age | create_version | delete_version |
+----+-----+----------+----------+
| 1 | 10 | 100 | NULL |
+----+-----+----------+----------+
事务 A(version = 100):
SELECT * FROM users WHERE id = 1; -- 读到 age = 10
事务 B(version = 101):
UPDATE users SET age = 20 WHERE id = 1;
-- 插入新版本:
INSERT INTO users VALUES (1, 20, 101, NULL);
-- 标记旧版本为已删除:
UPDATE users SET delete_version = 101 WHERE id = 1 AND create_version = 100;
事务 A(version = 100):
SELECT * FROM users WHERE id = 1; -- 还是读到 age = 10(读快照!)5.2 READ COMMITTED 和 REPEATABLE READ 的区别
| 隔离级别 | 快照创建时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每次读操作时创建新快照 | 能读到已提交的最新数据 |
| REPEATABLE READ | 事务开始时创建快照,事务期间都用这个快照 | 读到的数据始终一致 |
六、Next-Key Lock(临键锁)的原理
Next-Key Lock 是 MySQL InnoDB 防止幻读的机制。
6.1 什么是幻读?
事务 A:
START TRANSACTION;
SELECT * FROM users WHERE age > 10; -- 返回 2 行
-- 此时,事务 B 插入了一条 age = 20 的记录,并提交
SELECT * FROM users WHERE age > 10; -- 返回 3 行(幻读!)6.2 Next-Key Lock 如何防止幻读?
Next-Key Lock = 记录锁(Record Lock)+ 间隙锁(Gap Lock)
索引:1, 5, 10, 15, 20
Next-Key Lock 锁住:
- 记录本身(Record Lock)
- 记录之前的间隙(Gap Lock)
比如:SELECT * FROM users WHERE age > 10 FOR UPDATE;
会锁住:
- age = 15 的记录
- age = 20 的记录
- age > 20 的间隙
这样,其他事务就无法插入 age > 10 的记录,从而防止了幻读!七、扩展点总结
在这篇文章中,我们提到了以下几个扩展点:
| 扩展点 | 场景 | 如何扩展 |
|---|---|---|
| 严格校验已有事务的隔离级别 | 你想让隔离级别必须一致 | 设置 validateExistingTransaction = true |
| 自定义隔离级别设置逻辑 | 你想用其他方式设置隔离级别 | 继承 DataSourceTransactionManager,重写 doBegin() 方法 |
| 使用 READ_ONLY hint | 你想优化只读事务 | 在事务定义中设置 readOnly = true |
| 选择适合的隔离级别 | 平衡性能与一致性 | 根据业务需求选择(参看前面的表格) |
八、小结与伏笔
好了,到这里,我们已经把事务隔离级别的"神秘面纱"揭开了。
让我们来回顾一下整个流程:
- 当调用一个事务方法时,
TransactionInterceptor.invoke()会被调用 invoke()方法调用了TransactionAspectSupport.invokeWithinTransaction()方法invokeWithinTransaction()方法调用了createTransactionIfNecessary()方法createTransactionIfNecessary()方法调用了getTransaction()方法getTransaction()方法调用了doBegin()方法(如果新建事务)doBegin()方法调用了DataSourceUtils.prepareConnectionForTransaction()方法prepareConnectionForTransaction()方法调用了con.setTransactionIsolation()方法- 这就是 Spring 设置隔离级别的地方!
🔮 伏笔:连接是如何被"绑定"到当前线程的?
你可能会注意到,doBegin() 方法中有一行:
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());这行代码是做什么的?
- 连接是如何被"绑定"到当前线程的?
- 后续的操作是如何获取到这个连接的?
- MyBatis 执行 SQL 时,是如何获取到这个连接的?
这些问题,我们会在下一篇文章(系列一第5篇:事务管理器的设计哲学)中继续讲解。
下一篇预告:《Spring 事务(五):事务管理器的设计哲学》
在这篇文章中,我们会讲解:
PlatformTransactionManager接口的设计哲学DataSourceTransactionManager的源码实现- 事务资源的绑定与传递机制
- 如何自定义事务管理器
写在最后:这篇文章比较长,但我觉得值得。因为只有当理解了事务隔离级别的原理,你才能真正理解 Spring 事务的工作原理。如果你有什么疑问,欢迎留言讨论!