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 usersSELECT * FROM users两个事务读到自己的快照,不冲突(靠 MVCC)
并发写UPDATE users SET name=‘A’ WHERE id=1UPDATE users SET name=‘B’ WHERE id=1第二个事务会阻塞,等待锁释放(靠锁)
读后更新SELECT * FROM users WHERE id=1 FOR UPDATEUPDATE 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 years old.

Enjoy!

Contact me:

Email | GitHub


Content licenced under CC BY-NC-ND 4.0