•
4 min read

pgsql中的事务、并发和锁

都是AI写的,内容可能不准确

Table of Contents

🧠 什么是 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,写靠锁;要串行,二者全用。”


Jason Lee
Hi, I'm Jason Lee

I hope I can still be curious about the world when I turn 60.

Enjoy!

Contact me:

Email | GitHub


Content licenced under CC BY-NC-ND 4.0