数据库事务系列 · 第 3 篇
引言:一个「诡异」的现象
在前两篇文章中,我们做了一个完整的演示:
- 第 1 篇:我们学会了事务的基本操作(BEGIN、COMMIT、ROLLBACK、SAVEPOINT),并验证了原子性和持久性。
- 第 2 篇:我们把隔离级别调来调去,亲眼看到了脏读、不可重复读、幻读,以及四种隔离级别如何应对它们。
但你有没有想过一个问题------
在演示三(Repeatable Read) 中,我们看到了一个「诡异」的现象:
| 时间 | 终端B(柜员小李) | 终端A(柜员小张) | 张三余额 |
|---|---|---|---|
| T1 | BEGIN; |
100 | |
| T2 | SELECT balance FROM account WHERE id = 1; |
读到 100 | |
| T3 | BEGIN; |
||
| T4 | UPDATE account SET balance = 0 WHERE id = 1; |
||
| T5 | COMMIT; |
已提交,物理数据变为 0 | |
| T6 | SELECT balance FROM account WHERE id = 1; |
仍然读到 100 |
诡异的地方在于:
终端A 已经在 T5 时刻提交了事务,数据库里的「真实数据」已经变成了 0。但终端B 在 T6 时刻(自己的事务还没结束)读取到的,依然是 100。
终端B 凭什么能看到「已经不存在」的旧数据?
这个问题,指向了 MySQL 事务隔离性最核心的底层机制------
MVCC(Multi-Version Concurrency Control,多版本并发控制)。
一、MVCC 的核心思想:每个事务都有自己的「专属快照」
1.1 什么是 MVCC?
MVCC 的全称是 Multi-Version Concurrency Control,翻译过来就是「多版本并发控制」。
它的核心思想非常巧妙:
每次修改数据时,不直接覆盖旧数据,而是生成一个新版本。旧版本继续保留,供其他事务读取。
用生活场景来理解:
你在 Git 上提交代码,每次提交都会生成一个新的 commit。团队成员可以继续查看旧的 commit,也可以拉取最新的 commit。大家各看各的版本,互不干扰。
MVCC 在数据库里干的是同样的事情:
┌─────────────────────────────────────────────────────────────────┐
│ 数据行的版本链 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 版本 V1 (balance=100) ←── 事务B 正在读这个版本 │
│ │ │
│ ▼ (事务A 修改并提交) │
│ 版本 V2 (balance=0) ←── 物理数据现在是这个版本 │
│ │
│ 事务B 读到的还是 V1------因为事务B 启动时,V2 还没诞生 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 MVCC 解决了什么问题?
MVCC 让数据库实现了两个关键能力:
| 能力 | 说明 | 对应业务价值 |
|---|---|---|
| 读写不互斥 | 读操作不会阻塞写操作,写操作也不会阻塞读操作 | 高并发场景下,查询和更新可以同时进行 |
| 一致性读 | 每个事务都能读到「自己启动那一刻」的数据快照 | 事务内多次查询结果一致(可重复读) |
没有 MVCC 之前,读操作需要加锁,写操作也要加锁------读会阻塞写,写也会阻塞读,并发性能很差。有了 MVCC,读操作直接读取「旧版本」,不需要等写操作完成。
1.3 MVCC 的适用边界
MVCC 解决了「读写冲突 」,但解决不了「写写冲突」。
如果两个事务同时修改同一行数据(比如终端A 和终端C 同时要把张三的余额改为 0 和 50),MVCC 就无能为力了------因为最终只能有一个版本是「最新」的。
这个冲突,由锁机制来处理。MVCC 和锁,一个管「读-写」,一个管「写-写」,共同构建了完整的并发控制体系。
完整的分工图:

二、MVCC 的三大核心组成部分
在深入细节之前,我们先看一张完整的 MVCC 工作流程图。
这张图把隐藏字段、undo log 版本链、read view 三者的关系一次性展现出来。接下来的三小节,我们会逐一拆解图中的每一个部件。

2.1 隐藏字段:每行数据都有的「身份证」
在 InnoDB 中,每一行数据除了我们定义的字段(id、name、balance),还有三个隐藏字段:
| 隐藏字段 | 名称 | 大小 | 存储内容 |
|---|---|---|---|
| DB_TRX_ID | 事务ID | 6 字节 | 最近一次修改该行的事务ID |
| DB_ROLL_PTR | 回滚指针 | 7 字节 | 指向 Undo Log 中旧版本的指针 |
| DB_ROW_ID | 行ID | 6 字节 | 单调递增的行ID(无主键时用于生成聚集索引) |
我们通常不用关注 DB_ROW_ID,但 DB_TRX_ID 和 DB_ROLL_PTR 是理解 MVCC 的关键。
用「张三的余额」来看:

trx_id 是自增的,意味着什么?
| 规则 | 含义 |
|---|---|
| trx_id 越小 | 该版本被创建得越早 |
| trx_id 越大 | 该版本被创建得越晚 |
| 当前事务的 trx_id | 就是当前事务在系统中的唯一编号 |
这就是 read view 判断「谁先谁后」的依据------用 trx_id 的大小关系,判断数据版本和当前事务的时间先后。
2.2 Undo Log 链:数据版本的「全家桶」
每次修改数据时,InnoDB 会把修改前的旧版本 记录到 Undo Log 中。多个旧版本通过 roll_pointer 串联成一条版本链。
关键理解:多个事务并行操作同一行时,每个版本都记录着「谁改的」,并通过 roll_pointer 串联成一个链表。

每个版本都记录了两个关键信息:
- 是谁(哪个事务ID)创建了这个版本
- 上一个版本在哪里(DB_ROLL_PTR)
| 作用 | 说明 |
|---|---|
| 支持回滚 | 事务回滚时,沿着版本链找到旧版本,恢复数据 |
| 支持 MVCC | read view 沿着版本链遍历,找到「可见」的版本 |
| 支持一致性读 | RR 级别下,始终读取事务开始时的版本快照 |
2.3 Read View:事务启动时的「快照相机」
Read View 是 MVCC 中最重要的概念。它的作用是:
在事务执行
SELECT时,生成一个「快照」,记录当时系统中所有活跃事务的状态。
Read View 包含三个核心字段:
| 字段 | 含义 |
|---|---|
| min_trx_id | 当前所有活跃事务中,最小的 ID |
| max_trx_id | 系统下一个要分配的事务 ID(边界值) |
| m_ids | 当前所有活跃事务的 ID 列表(即正在进行中、还没提交的事务) |
Read View 生成后,数据可见性的判断规则:
当要读取某行数据时,看该行的 DB_TRX_ID(创建该版本的事务ID):

三、RR 与 RC 的核心差异:Read View 生成时机不同
这是理解「为什么 RR 能可重复读,而 RC 不能」的关键。
3.1 Repeatable Read:一个事务只用一张「快照」
在 RR 级别下:
Read View 在事务中第一次执行 SELECT 时生成,之后一直复用,直到事务结束。
这就是终端B 在演示三中「始终读到 100」的原因:
| 时间 | 终端B | Read View 状态 |
|---|---|---|
| T1 | BEGIN; |
事务开始,但还没生成 Read View |
| T2 | SELECT ...(读到 100) |
生成 Read View,记录了当时的数据快照 |
| T3 | 终端A 修改并提交(100 → 0) | 事务已提交,但 Read View 不变 |
| T4 | SELECT ...(仍然读到 100) |
复用同一个 Read View,仍然看到旧快照 |
┌─────────────────────────────────────────────────────────────────┐
│ RR 级别:Read View 复用示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 事务B 时间线 │
│ ────────────────────────────────────────────────► │
│ BEGIN 第一次 SELECT 第二次 SELECT COMMIT │
│ │ │ │ │ │
│ │ ┌────▼────┐ ┌────▼────┐ │ │
│ │ │生成 │ │复用 │ │ │
│ │ │Read View│ │Read View│ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ │ │ │ │
│ 数据快照: 100 100 100 → 事务结束 │
│ │
│ 即使终端A 在中间提交了修改(0),终端B 看到的始终是 100 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 Read Committed:每次 SELECT 都生成一张新「快照」
在 RC 级别下:
每次执行 SELECT,都重新生成一个 Read View。
这就是为什么 RC 会出现「不可重复读」------每次查询都看到最新的已提交数据。
| 时间 | 终端B | Read View 状态 |
|---|---|---|
| T1 | BEGIN; |
事务开始 |
| T2 | SELECT ...(读到 100) |
生成 Read View #1 |
| T3 | 终端A 修改并提交(100 → 0) | 事务已提交 |
| T4 | SELECT ...(读到 0) |
重新生成 Read View #2,看到了最新的已提交数据 |
四、锁机制:处理「写-写冲突」的唯一武器
MVCC 解决了「读-写冲突」,但「写-写冲突」必须由锁机制来处理。
4.1 为什么需要锁?
想象这个场景:
柜员小张(终端A)正在修改张三的余额:100 → 0(事务未提交)
柜员小王(终端C)也在修改张三的余额:100 → 50(事务未提交)
如果两个事务同时修改同一行数据,最终结果应该是 0 还是 50?
MVCC 解决不了这个问题,因为最终只能有一个版本成为「最新版本」。所以,第二个修改的事务必须等待第一个修改的事务完成(提交或回滚)。
这就是锁的作用------让「写-写」操作排队执行。
4.2 锁的分类(按粒度)
锁的「粒度」是指锁定的范围大小。粒度越大,锁定的数据越多,并发性能越差;粒度越小,锁定的数据越少,并发性能越好。

4.2.1 表级锁(Table Lock)
锁定整张表,粒度最大,并发最差。
场景: 在执行 ALTER TABLE、DROP TABLE 等 DDL 操作时,会加表级锁。
类比:把整个仓库锁上,所有人都不能进出。
4.2.2 行级锁(Row Lock)
锁定某一行记录,粒度最小,并发最好。
场景: InnoDB 在执行 UPDATE ... WHERE id = 1 时,只锁定 id=1 的这一行,其他行不受影响。
类比:只锁住仓库里的某一个货架,其他人还能搬其他货架的东西。
4.2.3 页级锁(Page Lock)
锁定一个数据页(通常 16KB),介于表锁和行锁之间。
说明: MySQL InnoDB 主要使用行锁,页级锁在一些其他数据库中更常见(如 SQL Server)。
4.3 锁的分类(按模式)
| 锁模式 | 英文 | 俗称 | 作用 |
|---|---|---|---|
| 共享锁 | Shared Lock | 读锁(S锁) | 允许其他事务读取,不允许修改 |
| 排他锁 | Exclusive Lock | 写锁(X锁) | 不允许其他事务读取或修改 |
4.3.1 共享锁(S锁,读锁)
多个事务可以同时持有共享锁,但不能修改数据。
- 加了 S 锁的数据,其他事务可以读 ,但不能写。
- 其他事务也可以加 S 锁,但不能加 X 锁。
加锁方式:
sql
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
业务场景: 读取数据用于生成报表。报表生成过程中,数据可以被其他人读,但不希望被修改(防止报表数据不一致)。
4.3.2 排他锁(X锁,写锁)
只允许一个事务持有排他锁,其他事务既不能读也不能写。
- 加了 X 锁的数据,其他事务不能读 (会被阻塞),也不能写。
- 在事务提交或回滚之前,X 锁不会释放。
加锁方式:
sql
-- 自动加 X 锁(任何修改操作都会自动加 X 锁)
UPDATE account SET balance = 0 WHERE id = 1;
-- 手动加 X 锁(SELECT ... FOR UPDATE)
SELECT * FROM account WHERE id = 1 FOR UPDATE;
业务场景: 查询某条数据并准备修改。在查询和修改之间,不希望其他事务修改这条数据。
4.4 共享锁 vs 排他锁 兼容性矩阵
| 锁类型 | 共享锁(S) | 排他锁(X) |
|---|---|---|
| 共享锁(S) | ✅ 兼容 | ❌ 冲突 |
| 排他锁(X) | ❌ 冲突 | ❌ 冲突 |
一句话记住:
- 多个读可以同时进行(S 和 S 兼容)
- 只要有写,其他人都不能动(X 和谁都冲突)
4.5 意向锁(Intention Lock):表级锁和行级锁的「协调员」
意向锁是 InnoDB 中一个非常重要但容易被忽视的概念。它的作用非常纯粹:
意向锁是一种「表级锁」,用于表示一个事务「打算」在表中的某些行加锁。
为什么需要意向锁?
想象一个场景:
- 事务A 正在修改
account表中的某一行(加行级 X 锁) - 事务B 想要锁定整张
account表(加表级锁)
如果没有意向锁,事务B 需要扫描所有行,检查是否有行级锁存在------这效率极低。
有了意向锁,事务A 在加行锁之前,会先给表加一个「意向锁」作为标记。事务B 看到表上有意向锁,就知道「表里已经有行锁了」,直接等待即可。
意向锁的两种类型:
| 意向锁 | 简称 | 含义 |
|---|---|---|
| 意向共享锁 | IS 锁 | 事务打算给某些行加共享锁(S 锁) |
| 意向排他锁 | IX 锁 | 事务打算给某些行加排他锁(X 锁) |
加锁顺序(重点):
事务要加行级 X 锁
│
▼
先加表级 IX 锁(意向排他锁)
│
▼
再加行级 X 锁(排他锁)
意向锁的兼容性:
| 锁类型 | 共享锁(S) | 排他锁(X) | 意向共享(IS) | 意向排他(IX) |
|---|---|---|---|---|
| 共享锁(S) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
| 排他锁(X) | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 |
| 意向共享(IS) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ✅ 兼容 |
| 意向排他(IX) | ❌ 冲突 | ❌ 冲突 | ✅ 兼容 | ✅ 兼容 |
关键点:意向锁之间是互相兼容的。 多个事务可以同时持有 IX 锁(表示多个事务都在修改不同行),但一旦有事务要加表级 X 锁,就必须等待所有意向锁释放。
4.6 行锁的三种具体类型(InnoDB 特有)
InnoDB 的行锁不是一种,而是三种,分别应对不同的场景:
4.6.1 记录锁(Record Lock)
锁定单个索引记录。
sql
-- 锁定 id=1 这一行
SELECT * FROM account WHERE id = 1 FOR UPDATE;
只锁住
id=1这一条记录,其他 id 的记录不受影响。
业务场景: 柜员查询张三的余额并准备修改,防止其他事务在查询和修改之间改变数据。
4.6.2 间隙锁(Gap Lock)
锁定索引记录之间的「间隙」,防止其他事务在该间隙插入数据。
sql
-- 假设 account 表中有 id: 1, 5, 10
SELECT * FROM account WHERE id BETWEEN 1 AND 10 FOR UPDATE;
间隙锁会锁定 (1, 5) 和 (5, 10) 之间的空隙,阻止其他事务插入 id=3 或 id=7 的新记录。
间隙锁的主要目的:防止幻读------阻止其他事务在查询范围内插入新数据。
业务场景: 柜员小李统计「余额大于 0 的账户总数」,不希望统计过程中有新账户开户(插入),否则统计数据就不准了。
4.6.3 Next-Key Lock
记录锁 + 间隙锁的组合。
Next-Key Lock = Record Lock + Gap Lock
它既锁住了记录本身,又锁住了记录前面的间隙。
在 MySQL 的 RR(可重复读)级别下,查询使用索引时,默认加的就是 Next-Key Lock。这就是为什么 MySQL 的 RR 级别能避免大部分幻读的原因。
五、三种行锁的详细说明与案例

5.1:记录锁(Record Lock)
目标 :证明记录锁只锁住 id=1 这一行,其他行不受影响。
Step 1:终端A 锁定 id=1
mysql
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
Step 2:终端B 修改 id=1(被阻塞)
mysql
BEGIN;
UPDATE account SET balance = 999 WHERE id = 1;

Step 3:终端B 修改 id=5(不受影响)
先 Ctrl+C 取消上一条被阻塞的语句,然后在终端B 执行:
mysql
UPDATE account SET balance = 888 WHERE id = 5;
📸 截图 3 :显示 Query OK, 1 row affected------立即成功。
Step 4:终端A 提交,释放锁
mysql
COMMIT;
此时终端B 被阻塞的 UPDATE 会立即执行成功。
结论 :记录锁只锁住 id=1 这一行,id=5 不受影响。

5.2:间隙锁(Gap Lock)
目标 :证明范围查询会锁住 (1,5) 之间的间隙,阻止在间隙内插入数据。
Step 1:重置数据
mysql
DELETE FROM account;
INSERT INTO account VALUES (1, '张三', 100);
INSERT INTO account VALUES (5, '李四', 200);
INSERT INTO account VALUES (10, '王五', 300);
Step 2:终端A 执行范围查询,加锁
mysql
BEGIN;
SELECT * FROM account WHERE id BETWEEN 1 AND 5 FOR UPDATE;

此时间隙锁锁住了 (1,5) 之间的空隙------也就是 id=2、3、4 的位置。**
Step 3:终端B 在间隙内插入(被阻塞)
mysql
BEGIN;
INSERT INTO account VALUES (3, '赵六', 400);

Step 4:终端B 在间隙外插入(不受影响)
先 Ctrl+C 取消,然后执行:
mysql
INSERT INTO account VALUES (7, '赵六', 400);
Step 5:终端A 提交
mysql
COMMIT;
结论 :间隙锁锁住了 (1,5) 之间的空隙,阻止在间隙内插入数据(防止幻读)。

5.3:Next-Key Lock
目标 :证明 SELECT ... WHERE id = 5 FOR UPDATE 不仅锁住了 id=5 这一行,还锁住了它前面的间隙 (1,5)。
Step 1:重置数据
mysql
DELETE FROM account;
INSERT INTO account VALUES (1, '张三', 100);
INSERT INTO account VALUES (5, '李四', 200);
INSERT INTO account VALUES (10, '王五', 300);
Step 2:终端A 锁定 id=5
mysql
BEGIN;
SELECT * FROM account WHERE id BETWEEN 1 AND 5 FOR UPDATE;
Step 3:终端B 修改 id=5(被阻塞------记录锁效果)
mysql
BEGIN;
INSERT INTO account VALUES (3, '赵六', 400);


Step 4:终端B 在间隙 (1,5) 中插入(被阻塞------间隙锁效果)
先 Ctrl+C 取消,然后执行:
mysql
INSERT INTO account VALUES (3, '赵六', 400);

Step 5:终端B 在间隙外插入(不受影响)
先 Ctrl+C 取消,然后执行:
mysql
INSERT INTO account VALUES (6, '赵六', 400);
Step 6:终端A 提交
mysql
COMMIT;

结论 :Next-Key Lock = 记录锁(锁住 id=5)+ 间隙锁(锁住前面的间隙 (1,5))。
三种行锁演示效果对比表
| 演示 | 终端A 的操作 | 终端B 的操作 | 结果 | 说明 |
|---|---|---|---|---|
| 记录锁 | SELECT ... WHERE id=1 FOR UPDATE |
UPDATE ... WHERE id=1 |
❌ 阻塞 | 记录锁锁住 id=1 |
UPDATE ... WHERE id=5 |
✅ 成功 | id=5 不受影响 | ||
| 间隙锁 | SELECT ... WHERE id BETWEEN 1 AND 5 FOR UPDATE |
INSERT ... id=3 |
❌ 阻塞 | 3 在间隙 (1,5) 中 |
INSERT ... id=7 |
✅ 成功 | 7 不在间隙中 | ||
| Next-Key Lock | SELECT ... WHERE id=5 FOR UPDATE |
UPDATE ... WHERE id=5 |
❌ 阻塞 | 记录锁 |
INSERT ... id=3 |
❌ 阻塞 | 间隙锁(前面的间隙) | ||
INSERT ... id=6 |
✅ 成功 | 6 不在锁范围内 |
六、不同隔离级别下的锁策略对比
这是本文最重要的总结表,建议收藏:
| 隔离级别 | 使用的锁机制 | 特点 | 并发性能 |
|---|---|---|---|
| Read Uncommitted | 几乎不加锁(读操作不加锁,写操作加 X 锁) | 性能最高,但脏读、不可重复读、幻读都可能 | 最高 |
| Read Committed | 读操作不加锁(MVCC),写操作加 X 锁;无间隙锁 | 避免脏读,但不可重复读、幻读可能 | 较高 |
| Repeatable Read | 读操作不加锁(MVCC),写操作加 X 锁;有间隙锁(Next-Key Lock) | 避免脏读、不可重复读,基本避免幻读 | 中等 |
| Serializable | 读操作加 S 锁,写操作加 X 锁;所有操作串行执行 | 避免所有并发问题,性能最低 | 最低 |
关键差异说明:
1. Read Committed 没有间隙锁,而 Repeatable Read 有
这就是为什么 RC 会出现幻读,而 RR 不会(在大多数场景下)。
我整理了一个简要对比表:
| 对比维度 | RC | RR |
|---|---|---|
| 是否使用 MVCC | ✅ 是 | ✅ 是 |
| Read View 生成时机 | 每次 SELECT | 第一次 SELECT |
| 是否使用间隙锁 | ❌ 否 | ✅ 是 |
| 能否避免幻读 | ❌ 不能 | ✅ 基本能 |
七、MVCC + 锁 + 日志:三者如何协作
7.1 三者的职责分工
| 机制 | 负责什么 | 解决什么问题 |
|---|---|---|
| Undo Log | 存储数据的历史版本 | 原子性(回滚)+ MVCC 的数据来源 |
| MVCC(Read View) | 控制事务能看到哪个版本 | 隔离性中的「读-写」冲突 |
| 锁(Lock) | 控制事务的并发修改 | 隔离性中的「写-写」冲突 |
| Redo Log | 记录修改操作 | 持久性(崩溃恢复) |
7.2 一个完整的「查询」流程(RR 级别下)
当终端B 执行 SELECT * FROM account WHERE id = 1; 时:

7.3 一个完整的「更新」流程(所有级别下)
当终端A 执行 UPDATE account SET balance = 0 WHERE id = 1; 时:

7.4 三者协作的完整视图

八、全文总结
8.1 核心知识点回顾
| 知识点 | 核心内容 | 一句话记忆 |
|---|---|---|
| MVCC | 多版本并发控制,通过 Undo Log 链 + Read View 实现 | 每个事务都有自己的数据快照 |
| Read View | 事务启动时记录活跃事务列表,决定数据可见性 | RR 级别只用一张快照,RC 每次重新拍 |
| Undo Log 链 | 数据行的历史版本串联成链,供 MVCC 读取 | 每个版本都记录着「谁改的」和「上一个」 |
| 记录锁 | 锁定单行,粒度最小 | 只锁这一条 |
| 间隙锁 | 锁定索引间隙,防止插入 | RR 级别用来防幻读 |
| Next-Key Lock | 记录锁 + 间隙锁 | RR 级别的默认行锁 |
| 意向锁 | 表级锁,标记表内有行锁 | 让表锁和行锁能协调工作 |
8.2 隔离级别与底层实现对照表
| 隔离级别 | MVCC 使用 | Read View 策略 | 间隙锁使用 | 如何保证可重复读 |
|---|---|---|---|---|
| Read Uncommitted | ❌ 不用 | 无 | ❌ 不用 | 不保证 |
| Read Committed | ✅ 使用 | 每次 SELECT 生成 | ❌ 不用 | 不保证 |
| Repeatable Read | ✅ 使用 | 首次 SELECT 生成 | ✅ 使用 | 固定 Read View + 间隙锁 |
| Serializable | ⚠️ 退化 | 无(串行化) | ✅ 使用 | 强制串行执行 |
8.3 一句话全文总结
MVCC 为每个事务创建「专属快照」,让读写互不干扰;锁机制在「写-写」冲突时强制排队,保证数据最终一致;Undo Log 提供历史版本,Redo Log 保证崩溃恢复。四者共同撑起了事务隔离性的整片天空。
📌 下篇预告
前三篇文章中,我们从 ACID 聊到了 MVCC,从脏读聊到了间隙锁------理论已经讲得够多了。
但实际开发中,我们该如何选择隔离级别?死锁是怎么产生的?怎么排查?用 Prisma ORM 写事务时有哪些坑?
下一篇文章(第 4 篇) ,我们将走进生产环境,聊聊事务的实战经验。
- 不同业务场景如何选隔离级别
- 死锁的成因、排查与预防
- 大事务的危害与拆分策略
- Prisma ORM 中的事务操作实践
| 无 | ❌ 不用 | 不保证 |
| Read Committed | ✅ 使用 | 每次 SELECT 生成 | ❌ 不用 | 不保证 |
| Repeatable Read | ✅ 使用 | 首次 SELECT 生成 | ✅ 使用 | 固定 Read View + 间隙锁 |
| Serializable | ⚠️ 退化 | 无(串行化) | ✅ 使用 | 强制串行执行 |
8.3 一句话全文总结
MVCC 为每个事务创建「专属快照」,让读写互不干扰;锁机制在「写-写」冲突时强制排队,保证数据最终一致;Undo Log 提供历史版本,Redo Log 保证崩溃恢复。四者共同撑起了事务隔离性的整片天空。
📌 下篇预告
前三篇文章中,我们从 ACID 聊到了 MVCC,从脏读聊到了间隙锁------理论已经讲得够多了。
但实际开发中,我们该如何选择隔离级别?死锁是怎么产生的?怎么排查?用 Prisma ORM 写事务时有哪些坑?