事务与隔离级别
开篇:为什么银行转账不能只转一半?
想象你站在 ATM 机前,准备把 100 元从 A 卡转到 B 卡。操作过程实际上是 6 个步骤:
- 从 A 账户读取余额(500)
- A 账户减去 100(500 - 100 = 400)
- 把 400 写回 A 账户
- 从 B 账户读取余额(500)
- B 账户加上 100(500 + 100 = 600)
- 把 600 写回 B 账户
如果执行到第 3 步之后突然断电,A 卡扣了 100,B 卡一分没多,钱凭空蒸发了。
银行系统当然不会允许这种事发生。它保证了"要么 6 步全做完,要么全不做"。数据库里管这件事的机制,叫做 事务(Transaction)。
在 MySQL 中,只有 InnoDB 引擎支持事务(可以通过 SHOW ENGINES; 确认)。本篇围绕事务展开,搞清楚三个核心问题:
- 事务靠什么保证数据不出错?(ACID 四特性)
- 多个事务同时跑,互相能看到什么?(四种隔离级别)
- MySQL 怎么做到"读不阻塞写"的?(MVCC 多版本并发控制)
一、事务的 ACID 特性
ACID 是事务必须满足的四个约束,我们继续用 ATM 转账来逐一理解。
1.1 原子性(Atomicity)—— 要么全做,要么全不做
上面的 6 个步骤是一个不可分割的"原子"操作。只要中间任何一步失败,数据库就会自动回滚所有已执行的操作,恢复到转账前的状态。
没有原子性会怎样?A 卡减了 100,B 卡加钱失败,系统凭空丢了 100 元。
实现手段:undo log。 每次修改数据前,先把旧值记到 undo log 里。需要回滚时,按 undo log 逆向执行操作即可恢复原样。比如 INSERT 了一行,undo log 记录的就是对应的 DELETE。
1.2 一致性(Consistency)—— 数据始终满足业务规则
转账前 A + B = 1000 元,转账后 A + B 依然 = 1000 元。余额不能变成负数,总额不能凭空增减。
一致性不是指语法上的一致,而是 语义上的合法。什么算"合法"由业务定义:比如账户余额不能为负、库存不能超卖、年龄不能是负数。
一致性是事务的最终目标,原子性、隔离性和持久性是实现它的三种手段。数据库的主键约束、外键约束、唯一约束、非空约束等机制也在协助保证一致性。
1.3 隔离性(Isolation)—— 事务之间互不干扰
你在 ATM 转账的时候,隔壁窗口的小李在查询 B 卡余额。他不应该看到"A 扣了钱但 B 还没入账"的中间状态——否则他可能误以为 B 卡余额比实际少。
隔离性就是要保证:一个事务执行过程中的中间状态对其他并发事务不可见。
还有一种情况:你在给 B 转账的同时,小王也在给 B 转账。两个事务都结束后,B 的余额应该是原始值 + 你转的钱 + 小王转的钱。不能出现"覆盖写"。
实现手段:锁机制 + MVCC。 不同的隔离级别对应不同强度的隔离策略。
1.4 持久性(Durability)—— 提交了就永久生效
一旦事务成功提交,数据的变更就是永久性的。即使提交后一秒钟就断电、宕机,重启后数据依然存在。
实现手段:redo log。 事务提交时先将修改记录到 redo log 并刷盘。即使 Buffer Pool 中的脏页还没来得及写到数据文件,重启后也能根据 redo log 重做恢复。这种策略叫 WAL(Write-Ahead Logging)——先写日志,再写数据。
ACID 实现机制速查
| 特性 | 核心实现 |
|---|---|
| 原子性 | undo log(回滚日志) |
| 一致性 | 原子性 + 隔离性 + 持久性共同保障,配合数据库约束 |
| 隔离性 | 锁(共享锁、排他锁、间隙锁)+ MVCC(版本链、ReadView) |
| 持久性 | redo log(重做日志)+ WAL 机制 |
一句话记忆
原子性是基础,隔离性是手段,持久性是保障,一致性是目标。
1.5 事务的生命周期
一个事务从出生到结束,会经历以下几种状态:
- 活动(Active):事务中的 SQL 正在执行
- 部分提交(Partially Committed):所有 SQL 执行完毕,但修改还在内存中
- 已提交(Committed):数据成功持久化到磁盘,事务完成
- 失败(Failed):执行中遇到错误,或被人为终止
- 已中止(Aborted):回滚完毕,数据恢复到事务开始前的状态
二、四大隔离级别
多个事务并发执行时,如果不做任何控制,就可能出现三种"读异常":
| 异常现象 | 含义 | 生活类比 |
|---|---|---|
| 脏读(Dirty Read) | 读到了别人还没提交的数据 | 偷看别人还没写完的考卷,他随时可能涂改答案 |
| 不可重复读(Non-repeatable Read) | 同一事务内两次读同一行,结果不同(被 UPDATE/DELETE) | 菜单上刚看到牛排 88 元,回头一看变成 128 了 |
| 幻读(Phantom Read) | 同一事务内两次范围查询,结果行数不同(被 INSERT) | 数了教室里 30 人,回头一数变成 31 人 |
不可重复读针对的是"同一行数据的值变了",幻读针对的是"行数变了"。前者由 UPDATE/DELETE 引起,后者由 INSERT 引起。
SQL-92 标准定义了四种隔离级别来应对这些异常,从低到高:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 最高 |
| 读已提交(Read Committed) | 已解决 | 可能 | 可能 | 较高 |
| 可重复读(Repeatable Read) | 已解决 | 已解决 | 可能 | 中等 |
| 串行化(Serializable) | 已解决 | 已解决 | 已解决 | 最低 |
以上是 SQL 标准的定义。不同数据库的实际实现有差异:InnoDB 的 RR 级别通过 MVCC + 间隙锁,已经解决了大部分幻读场景。
2.1 读未提交(Read Uncommitted):没有锁的世界
一个事务还没提交,它的修改就对所有人可见了。
这就像考试时直接抄邻座还没写完的答案——他可能随时涂改,你抄到的可能是错的。这就是 脏读。一旦邻座回滚了修改,你读到的数据就是"从未存在过的值"。
InnoDB 在这个级别下直接读取最新数据,不创建 ReadView,不做任何版本控制。
几乎没有生产环境会用这个级别。
2.2 读已提交(Read Committed):Oracle 的默认选择
只能读到别人已经提交的数据,脏读被消灭了。
但还有一个问题:同一事务内前后两次读同一行,可能得到不同的值——因为中间别人提交了新值。这就是 不可重复读。
Oracle 默认就是 RC。InnoDB 在 RC 下的实现方式是:每次 SELECT 都生成一个新的 ReadView,所以每次读都能看到最新已提交的数据。
RC 还支持一种叫 "半一致读"(Semi-consistent Read) 的优化:执行 UPDATE 时,如果 WHERE 条件匹配到的记录已被其他事务加了排他锁,InnoDB 不会傻等,而是返回该记录最近提交的版本,让 MySQL 上层判断是否真的需要锁定这行。这大大减少了更新语句的锁冲突。
2.3 可重复读(Repeatable Read):MySQL 的默认选择
在一个事务内,无论读多少次,看到的数据都和第一次读时一样。
InnoDB 的做法是:只在事务的第一次 SELECT 时生成 ReadView,后续所有快照读都复用这个 ReadView。因为 ReadView 不变,所以其他事务的提交对当前事务不可见——这就是"可重复读"的含义。
为什么 MySQL 选择 RR 而不是 RC 作为默认级别?
这有一个有趣的历史原因。MySQL 早期的 binlog 只支持 statement 格式(记录 SQL 原文),RC 下会因事务提交顺序与 binlog 记录顺序不一致导致主从数据不一致。
举个具体的例子:
-- 表 t1 有一条记录:(10, 1)
-- Session 1 (RC) -- Session 2 (RC)
BEGIN; BEGIN;
DELETE FROM t1 WHERE b < 100;
INSERT INTO t1 VALUES(10, 99);
COMMIT; -- Session 2 先提交
COMMIT; -- Session 1 后提交主库执行后表中只剩 (10, 99)。但 statement 格式的 binlog 按提交顺序记录:先 INSERT(Session 2 先提交),再 DELETE。从库回放后先插入 (10, 99) 再删除所有 b < 100 的行——表变空了。主从不一致。
RR 级别下 DELETE 会加间隙锁,Session 2 的 INSERT 被阻塞直到 Session 1 提交,保证了执行顺序一致性。这就是 MySQL 选择 RR 的根本原因。
现在 binlog 已经支持 row 格式(记录行数据的变更而非 SQL 原文),很多大厂会把默认级别改为 RC。原因有二:
- 提升并发:RC 不加间隙锁和临键锁,锁粒度更小,还支持半一致读
- 减少死锁:锁更少,死锁概率自然更低
RC 下不可重复读的问题通常可以用业务层乐观锁解决。
2.4 串行化(Serializable):绝对安全但代价最高
所有读操作自动加共享锁(相当于 SELECT ... LOCK IN SHARE MODE),所有写操作加排他锁。事务完全串行执行,不存在任何并发问题。
-- 事务 A(Serializable)
BEGIN;
SELECT * FROM users WHERE age > 20; -- 自动加共享锁
-- 事务 B(被阻塞)
UPDATE users SET name = 'Bob' WHERE age = 25;
-- 必须等事务 A 提交才能执行代价是性能极低,一般只在对一致性要求极高且并发量很低的场景中使用。
隔离级别常用命令
-- 查看当前会话隔离级别(MySQL 8.0+)
SELECT @@transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 手动开启事务
START TRANSACTION;
-- 或者关闭自动提交
SET autocommit = OFF;MySQL 8.0 之前
8.0 之前查看隔离级别用 SELECT @@tx_isolation;,8.0+ 改为 @@transaction_isolation。
三、MVCC:多版本并发控制
MVCC(Multi-Version Concurrency Control)是 InnoDB 实现高并发的核心武器。它的目标很明确:让读和写互不阻塞。
读数据时不需要加锁,写数据时也不阻塞读操作。这是通过给数据保留多个历史版本来实现的——读操作按规则选一个合适的历史版本即可。
快照读 vs 当前读
在理解 MVCC 之前,必须先区分两种读操作:
| 读类型 | 说明 | 对应 SQL | 是否加锁 |
|---|---|---|---|
| 快照读(Snapshot Read) | 读的是历史版本(快照) | 普通 SELECT | 不加锁 |
| 当前读(Current Read) | 读的是最新版本 | SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE | 加锁 |
MVCC 只作用于快照读。 当前读走的是加锁机制(悲观锁)。
可以说:快照读是 MVCC 实现的基础,当前读是悲观锁实现的基础。
RU 直接读最新数据,不需要 MVCC;Serializable 所有读都自动加锁(退化为当前读),也不用 MVCC。所以 MVCC 只在 RC 和 RR 两个隔离级别下工作。
3.1 隐藏列与版本链
InnoDB 的每行记录都偷偷带了几个隐藏列:
| 隐藏列 | 作用 |
|---|---|
db_trx_id | 最后一次修改这行数据的事务 ID |
db_roll_ptr | 回滚指针,指向 undo log 中这行数据的上一个版本 |
db_row_id | 隐藏主键(表没有显式定义主键时 InnoDB 自动生成) |
注意:这些隐藏列只存在于聚簇索引(主键索引)的行记录中,普通二级索引中没有。
每次对一条记录执行 UPDATE,InnoDB 会:
- 把修改前的旧数据写入 undo log
- 更新当前行的
db_trx_id为当前事务 ID - 把
db_roll_ptr指向刚写入的 undo log 记录
多次修改就形成了一条 版本链(由 db_roll_ptr 串起来的单链表):
INSERT 产生的 undo log 在事务提交后就可以删除了(只用于回滚)。但 UPDATE/DELETE 产生的 undo log 还要用于 MVCC 的快照读,必须等到没有活跃事务依赖它时,才会被后台的 purge 线程清理。
这也是为什么长事务会导致 undo log 膨胀——它的 ReadView 一直"钉"着老版本,阻止 purge 回收空间。
3.2 ReadView 的工作原理
有了版本链,下一个问题是:当前事务应该读版本链上的哪个版本?
答案是 ReadView。它就像一张"快照身份证",记录了创建那一刻系统中事务的状态信息。
ReadView 包含四个关键属性:
| 属性 | 含义 |
|---|---|
creator_trx_id | 创建这个 ReadView 的事务 ID |
trx_ids | 创建 ReadView 时系统中所有活跃(已开始但未提交)的读写事务 ID 列表 |
up_limit_id | trx_ids 中最小的事务 ID(低水位) |
low_limit_id | 创建 ReadView 时系统应该分配给下一个事务的 ID(高水位) |
命名有点反直觉:
up_limit_id是最小值,low_limit_id是最大值+1。这是 MySQL 源码中的命名习惯,别被绕晕了。
可见性判断规则(假设版本链中某个版本的 trx_id 为 T):
- T == creator_trx_id → 这是我自己修改的,当然可见
- T < up_limit_id → 这个事务在创建 ReadView 之前就已经提交了,可见
- T >= low_limit_id → 这个事务在创建 ReadView 之后才出现,不可见
- up_limit_id <= T < low_limit_id → 需要进一步判断:
- T 在
trx_ids列表中 → 创建 ReadView 时该事务还活跃(未提交),不可见 - T 不在
trx_ids列表中 → 创建 ReadView 之前已提交,可见
- T 在
一句话总结:一个事务能看到的,是在它开始之前就已经提交的事务的结果;未提交的结果都不可见。
如果当前版本不可见,就顺着 db_roll_ptr 到 undo log 中找更老的版本,用同样的规则再判断。一直找到可见的版本为止。如果遍历整条版本链都没有可见版本,说明这行记录对当前事务完全不可见,查询结果不包含该行。
3.3 RC vs RR 下 ReadView 的差异
这是 MVCC 最核心的区别,一句话就能概括:
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| RC | 每次 SELECT 都生成新的 ReadView | 每次读都能看到最新的已提交数据 |
| RR | 只在第一次 SELECT 时生成,后续复用 | 事务内看到的数据始终一致 |
用一个具体场景来感受差异。假设有一张 users 表:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(20),
age INT
);
INSERT INTO users VALUES (1, '张三', 25);两个事务并发执行:
| 时间线 | 事务 A | 事务 B |
|---|---|---|
| T1 | BEGIN; | |
| T2 | SELECT * FROM users WHERE id=1; → 张三, 25 | |
| T3 | BEGIN; | |
| T4 | UPDATE users SET age=30 WHERE id=1; | |
| T5 | COMMIT; | |
| T6 | SELECT * FROM users WHERE id=1; → ? |
在 RR 下:T6 读到的是 age=25。因为复用了 T2 时创建的 ReadView,事务 B 的 trx_id 在 trx_ids 中(T2 时事务 B 还未开始,但 T6 复用的是 T2 的 ReadView,此时事务 B 的 trx_id >= low_limit_id),所以不可见。
在 RC 下:T6 读到的是 age=30。因为重新生成了 ReadView,此时事务 B 已提交,它的 trx_id < 新 ReadView 的 up_limit_id,可见。
这就是为什么 RR 叫"可重复读"——同一事务内多次读,结果始终一致。
MVCC 整体工作流程
把上面的知识串起来,一次快照读的完整流程:
- 获取当前事务的事务 ID
- 获取 ReadView(RC 下每次都新建,RR 下首次新建后复用)
- 在聚簇索引中找到目标行,取出该行的
db_trx_id - 用 ReadView 的可见性规则判断该版本是否可见
- 如果不可见,顺着
db_roll_ptr到 undo log 中找更老的版本,重复步骤 4 - 返回第一个可见的版本数据(如果全部不可见则该行不出现在结果集中)
二级索引与 MVCC
一个细节值得注意:MVCC 的隐藏列(trx_id、roll_ptr)只存在于聚簇索引中,二级索引里没有这些信息。
那走索引覆盖的查询怎么做 MVCC?
InnoDB 在二级索引的每个数据页上维护了一个 PAGE_MAX_TRX_ID,记录修改过该页的最大事务 ID。如果当前 ReadView 的 up_limit_id 大于这个值,说明该页上所有修改都已提交,可以直接走索引覆盖。否则就需要回表到聚簇索引,通过版本链做 MVCC 判断。
四、幻读问题与解决方案
幻读是事务并发中最棘手的问题。InnoDB 的 RR 级别通过 MVCC + 间隙锁解决了大部分幻读,但不是全部。
4.1 快照读下的幻读
RR 级别下,普通 SELECT(快照读)通过 MVCC 天然避免了幻读。因为 ReadView 只生成一次且后续复用,别的事务新插入的行的 trx_id 必然 >= low_limit_id,不可见。
4.2 当前读下的幻读
当前读(SELECT ... FOR UPDATE、UPDATE、DELETE 等)读取的是最新版本,MVCC 帮不了忙。InnoDB 靠 间隙锁(Gap Lock) 来解决:
-- 事务 A
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE;
-- 对 age > 20 的所有记录及其间隙都加了 next-key lock
-- 事务 B(被阻塞)
INSERT INTO users VALUES (5, '赵六', 22);
-- 插入操作落在了被锁定的间隙中,必须等事务 A 释放锁间隙被锁住后,其他事务无法在该范围内插入新数据,从而避免了幻读。
4.3 RR 下仍然可能出现幻读的场景
虽然 MVCC + 间隙锁解决了大部分幻读,但有两个特殊场景仍然会"漏":
场景一:先快照读,再当前读
事务 A: BEGIN;
事务 A: SELECT * FROM users WHERE age > 20; -- 快照读,没加锁,结果 2 行
事务 B: INSERT INTO users VALUES (5, '赵六', 22); COMMIT; -- 趁机插入成功
事务 A: SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 当前读,3 行!幻读第一次是快照读,没加锁,所以事务 B 的插入没有被阻塞。第二次切到当前读,就看到了新行。
场景二:先快照读,再 UPDATE 别人新插入的行
事务 A: BEGIN;
事务 A: SELECT * FROM users WHERE age > 20; -- 快照读,2 行
事务 B: INSERT INTO users VALUES (5, '赵六', 22); COMMIT; -- 插入成功
事务 A: UPDATE users SET name='修改' WHERE id=5; -- UPDATE 是当前读,成功!
事务 A: SELECT * FROM users WHERE age > 20; -- 快照读,但本事务修改了该行
-- RR 下本事务的修改会更新快照
-- 结果 3 行!幻读UPDATE 是当前读,命中了事务 B 新插入的行。这行数据变成了"本事务修改过的",后续快照读也能看到了。
4.4 彻底避免幻读的方法
- 方案一:使用 Serializable 隔离级别,所有读加共享锁,完全串行执行(性能代价大)
- 方案二:在 RR 下,事务一开始就对目标范围使用
SELECT ... FOR UPDATE加锁,让间隙锁挡住新数据的插入 - 注意:间隙锁是死锁的重要来源之一,使用时要谨慎
五、常见面试题精选
Q1:InnoDB 如何解决脏读、不可重复读和幻读?
通过 MVCC 解决脏读和不可重复读,通过 MVCC + 间隙锁解决大部分幻读。
| 读异常 | 解决方式 |
|---|---|
| 脏读 | RC 级别下,每次查询生成新 ReadView,只读已提交的版本 |
| 不可重复读 | RR 级别下,事务内复用同一个 ReadView,看到的数据始终一致 |
| 幻读 | 快照读靠 MVCC 解决;当前读靠间隙锁阻止新数据插入 |
Q2:MVCC 的核心组件有哪些?各自的职责是什么?
三个核心组件协同工作:
- 隐藏列(
db_trx_id+db_roll_ptr):为每行数据建立版本链 - undo log:存储历史版本数据,是版本链的物理载体
- ReadView:判断版本链上哪个版本对当前事务可见
Q3:RC 和 RR 在 MVCC 上的区别是什么?
唯一区别:ReadView 的生成时机不同。 RC 每次 SELECT 都生成新的 ReadView,RR 只在第一次 SELECT 时生成并在事务生命周期内复用。
Q4:为什么 MySQL 默认 RR?为什么大厂改成 RC?
MySQL 默认 RR 是因为早期 binlog 只有 statement 格式,RC 下主从复制会出现数据不一致。
大厂改成 RC 是因为:(1) 锁更少、并发更高——RC 没有间隙锁,还支持半一致读;(2) 死锁概率更低。不可重复读问题通过业务层乐观锁解决,binlog 用 row 格式就不会有主从不一致问题。
Q5:大事务会带来什么问题?
- 长时间占用连接:拖垮连接池
- 回滚代价高:修改量大,回滚耗时长
- 主从延迟:主库执行慢,从库回放同样慢
- 锁竞争加剧:持锁时间长,其他事务排队等待
- undo log 膨胀:大事务的 ReadView "钉"着老版本,阻止 purge 回收
- binlog 风险:超过
max_binlog_cache_size直接报错
解决方案:拆分大事务为多个小事务,把不需要事务保护的操作(读操作、远程调用、内存计算)移到事务外。
Q6:SELECT 语句会用到事务吗?
会。在 InnoDB 中,即使没有显式 BEGIN,普通 SELECT 也会在隐式的自动提交事务中执行。不过由于没有修改操作,不会持有任何写锁,查询结束后立即提交。
小结
| 概念 | 一句话总结 |
|---|---|
| ACID | 原子性保底线,隔离性控并发,持久性防崩溃,一致性是终极目标 |
| 隔离级别 | 从 RU 到 Serializable,安全递增,并发递减。MySQL 默认 RR,Oracle 默认 RC |
| MVCC | 版本链 + ReadView,让读写不阻塞。RC 和 RR 的区别就在于 ReadView 的生成时机 |
| 幻读 | 快照读靠 MVCC,当前读靠间隙锁。混用两种读仍可能触发幻读 |
| 大事务 | 拆。把大事务拆小,把非事务操作移到事务外 |
附录:ReadView 可见性判断实战演练
光看规则容易晕,我们用一个具体数值的例子走一遍完整流程。
假设当前系统有以下事务状态:
| 事务 ID | 状态 |
|---|---|
| trx 3 | 已提交 |
| trx 5 | 活跃(未提交) |
| trx 6 | 活跃(未提交) |
| trx 7 | 当前事务(刚发起 SELECT) |
| trx 8 | 活跃(未提交) |
事务 7 执行 SELECT 时创建的 ReadView 为:
creator_trx_id = 7
trx_ids = [5, 6, 8]
up_limit_id = 5 (活跃事务中最小的 ID)
low_limit_id = 9 (下一个将分配的事务 ID)现在要读某行记录,版本链如下:
当前版本 (trx_id=8, name='赵六')
→ undo log (trx_id=6, name='王五')
→ undo log (trx_id=3, name='李四')
→ undo log (trx_id=1, name='张三')第 1 步: 检查当前版本 trx_id=8
- 8 != 7(不是自己改的)
- 8 >= 5 且 8 < 9,属于第 4 条规则
- 8 在 trx_ids [5,6,8] 中 → 创建 ReadView 时事务 8 还活跃 → 不可见
第 2 步: 沿 roll_ptr 找上一版本 trx_id=6
- 6 != 7
- 6 >= 5 且 6 < 9
- 6 在 trx_ids [5,6,8] 中 → 事务 6 还活跃 → 不可见
第 3 步: 继续找上一版本 trx_id=3
- 3 != 7
- 3 < 5(up_limit_id)→ 事务 3 在创建 ReadView 之前已提交 → 可见!
最终结果:事务 7 读到的是 name='李四'。
这个过程清楚地展示了 MVCC 如何通过 ReadView + 版本链,在不加锁的情况下让事务读到正确的历史版本。
附录:MVCC 与隔离级别的完整关系
MVCC 并不适用于所有隔离级别,只在 RC 和 RR 下生效:
| 隔离级别 | 是否使用 MVCC | 读取方式 | ReadView 策略 |
|---|---|---|---|
| Read Uncommitted | 否 | 直接读最新数据,无版本控制 | 不创建 ReadView |
| Read Committed | 是 | 快照读 | 每次 SELECT 新建 ReadView |
| Repeatable Read | 是 | 快照读 | 首次 SELECT 新建,后续复用 |
| Serializable | 否 | 所有读加共享锁(当前读) | 不创建 ReadView |
MVCC 解决了读写并发的问题(读不阻塞写,写不阻塞读),但无法解决写写并发——那得靠锁机制。
通过 MVCC,InnoDB 获得了三大收益:
- 读写互不阻塞:提升事务并发处理能力
- 降低死锁概率:读操作不加锁,减少了锁冲突
- 实现一致性快照:事务在某个时间点看到的数据是一致的,不受其他事务影响
附录:事务操作快速参考
-- 开启事务
START TRANSACTION;
-- 或者
BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
-- 设置保存点(允许部分回滚)
SAVEPOINT sp1;
-- 回滚到保存点
ROLLBACK TO SAVEPOINT sp1;
-- 释放保存点
RELEASE SAVEPOINT sp1;
-- 查看当前是否在事务中
SELECT @@autocommit; -- 1 表示自动提交,0 表示手动提交