🧠 什么是 MVCC?
MVCC(多版本并发控制):每次修改数据时不会覆盖原来的行,而是创建一个“新版本”,事务读取时只看到对它“可见”的版本。
🧠 核心思想:每个事务有自己的“快照视图”
每个事务开始时,PostgreSQL 会生成一个“快照”,记录:
- 当前已提交的事务;
- 正在运行中的事务;
- 自己的事务 ID。
然后在查询时,根据每行数据的版本元信息(xmin
, xmax
)来判断该行对当前事务是否“可见”。
📦 每一行数据的版本信息
每一行记录包含两个隐藏字段:
字段名 | 含义 |
---|---|
xmin | 创建该行的事务 ID(谁插入了这行) |
xmax | 删除或更新该行的事务 ID(谁废弃了这行) |
还有可能配合使用:
ctid
: 表示这行的位置,更新后会换位置;hint bits
: 加速判断是否可见(提高性能);
🔍 判断可见性的规则(简化版)
在事务 T 中读一行记录 R,要判断:
R 对事务 T 可见的条件:
1. R.xmin 已提交
2. R.xmin 在事务 T 开始前启动
3. R.xmax 为空 或 R.xmax 对事务 T 不可见(即更新/删除它的事务尚未提交)
这就保证了:
- 不看到未来数据(更新还没提交的);
- 不看到被删除的数据(删除还没提交);
- 只看到自己“开始时就存在”的世界。
🧪 举个实际例子
-- 假设表 users 有一行 id=1, name='Bob'
-- 事务 A 开始
BEGIN; -- A
SELECT * FROM users WHERE id = 1;
-- 此时事务 B 更新该行
BEGIN; -- B
UPDATE users SET name = 'Alice' WHERE id = 1;
COMMIT;
-- 事务 A 继续查询
SELECT * FROM users WHERE id = 1;
结果是:
- 事务 A 看到的是 Bob,而不是 Alice;
- 即使 B 已提交,A 的快照“早于 B”,所以 A 看不到 B 的改动。
✅ 读操作使用 MVCC 的好处
优点 | 说明 |
---|---|
非阻塞读 | 不需要加锁,也不会阻塞其他事务 |
一致性快照 | 多条查询之间看到的版本一致(取决于隔离级别) |
高并发性能 | 避免加锁争用,读写互不干扰 |
🧱 不同隔离级别下,MVCC 行为的变化
隔离级别 | 快照生成时间 | 是否能看到其他事务提交的数据 |
---|---|---|
READ COMMITTED | 每条语句执行时 | ✅ 是,可以看到已经提交的数据 |
REPEATABLE READ | 事务开始时 | ❌ 否,整个事务看到的是启动时的快照 |
SERIALIZABLE | 同上(但有冲突检测) | ❌ 否,最严格,还可能回滚事务 |
🎯 总结一句话
读操作中,MVCC 通过判断每行的“版本信息”(xmin/xmax)与当前事务的快照,决定该行是否对当前事务可见,从而实现非阻塞、隔离一致的并发读。
🧠 核心原则:MVCC 与锁的责任划分
功能 | MVCC 负责 | 锁机制 负责 |
---|---|---|
并发读写控制 | ✅ 是,非阻塞读 | ❌ 否 |
写写冲突控制 | ❌ 否 | ✅ 是(行级锁) |
事务隔离 | ✅ 是(版本可见性) | ✅ 是(Serializable 会用锁) |
防止“脏写” | ❌ 否 | ✅ 是 |
防止“幻读” | ❌ 否 | ✅ 是(需显式加锁,如 Gap Lock) |
并发数据一致性保障 | ✅ 是 | ✅ 是 |
✅ 依靠 MVCC 的操作
操作类型 | 是否加锁 | 是否阻塞 | 原因说明 |
---|---|---|---|
普通 SELECT 查询 | ❌ 不加锁 | ❌ 不阻塞 | 读取符合“版本可见性”的行 |
多事务并发 SELECT | ❌ 不加锁 | ❌ 不阻塞 | 每个事务看到自己的“快照” |
UPDATE 或 DELETE 被执行时的旧数据访问 | ❌ 不阻塞 | ❌ 不影响读 | 读者看到旧版本数据 |
MVCC 保证你看到的数据是“对你事务可见”的,不需要加锁。
✅ 依靠锁的操作
操作类型 | 锁种类 | 会不会阻塞 | 原因说明 |
---|---|---|---|
两个事务同时 UPDATE 同一行 | 行级排他锁(RowExclusive) | ✅ 是 | 防止写写冲突 |
SELECT … FOR UPDATE | 行级锁 | ✅ 是 | 需要等待已有锁释放 |
ALTER TABLE 等 DDL | 表级锁 | ✅ 是 | 防止表结构变化过程中被其他事务使用 |
SERIALIZABLE 级别下访问冲突 | 多种锁 | ✅ 是 | 保证严格串行化一致性 |
✅ 同时依赖 MVCC 和锁的操作
操作类型 | MVCC 用于 | 锁机制用于 |
---|---|---|
UPDATE / DELETE | 创建新版本、标记旧版本 | 获取行锁,防止其他事务也写 |
INSERT 产生主键冲突 | 写入新行、版本控制 | 唯一约束/索引锁防止冲突 |
SELECT … FOR UPDATE | 读取 MVCC 可见版本 | 加锁用于后续更新操作准备 |
SERIALIZABLE 模式下的读写操作 | 保持视图一致性 | 强化隔离级别,阻止并发写冲突 |
🧩 一张总结表:谁负责什么?
任务 | MVCC | 锁机制 |
---|---|---|
并发读操作 | ✅ | ❌ |
控制写冲突 | ❌ | ✅ |
保证读一致性 | ✅ | ❌ |
强制串行执行 | ❌ | ✅(如 Serializable) |
乐观并发控制 | ✅ | ❌ |
悲观并发控制 | ❌ | ✅(如 FOR UPDATE) |
事务回滚后的状态隔离 | ✅ | ❌ |
✅ 举例对比
场景 | 事务 A | 事务 B | 发生了什么 |
---|---|---|---|
并发读 | SELECT * FROM users | SELECT * FROM users | 两个事务读到自己的快照,不冲突(靠 MVCC) |
并发写 | UPDATE users SET name=‘A’ WHERE id=1 | UPDATE users SET name=‘B’ WHERE id=1 | 第二个事务会阻塞,等待锁释放(靠锁) |
读后更新 | SELECT * FROM users WHERE id=1 FOR UPDATE | UPDATE users SET name=‘B’ WHERE id=1 | 第二事务阻塞,因 FOR UPDATE 加了锁 |
插入重复主键 | INSERT INTO users(id) VALUES(1) | INSERT INTO users(id) VALUES(1) | 第二事务报错或阻塞(靠索引锁) |
✅ 总结口诀
“读靠 MVCC,写靠锁;要串行,二者全用。”
Content licenced under CC BY-NC-ND 4.0