MVCC 与快照读是什么
MVCC(多版本并发控制)让读与写互不阻塞:读操作看到的是一致性快照,写操作生成新版本而不是原地覆盖。快照的建立时机由隔离级别决定——READ COMMITTED 下每条语句取一个新快照,REPEATABLE READ 下整个事务共用同一个快照,这正是「同一个事务里两次相同的查询返回不同结果」的根源。MySQL InnoDB 默认 REPEATABLE READ,快照在事务首次读时建立,旧版本放在 undo log 中;PostgreSQL 默认 READ COMMITTED,旧版本就是堆表里的死元组,靠 VACUUM 回收。
基本信息
| MySQL InnoDB | 默认 REPEATABLE READ,一致性读快照在事务第一次读时建立 |
|---|---|
| PostgreSQL | 默认 READ COMMITTED,每条语句建立新快照 |
| 快照读 | 普通 SELECT,不加锁、不阻塞写,读的是历史版本 |
| 当前读 | SELECT … FOR UPDATE / FOR SHARE,读最新已提交版本并加锁 |
| 旧版本存放 | InnoDB:undo log(回滚段);PG:堆表内死元组,由 VACUUM 清理 |
典型使用场景
- 报表需要事务内一致性视图:多张表读出来必须对应同一时刻,用 REPEATABLE READ
- 排查「两次查询结果不同」:先确认隔离级别与快照建立时机
- 理解加锁读为什么能看到别人刚提交的数据:FOR UPDATE 走当前读
- 长事务排查:undo 无法截断、PG 死元组堆积都会表现为磁盘与延迟上升
- 读写分离场景:从库复制延迟叠加快照,读到的可能比预期更旧
MySQL 快照读演示
MySQL 快照读演示
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; # 300
# 此时另一个事务把余额更新为 500 并提交
SELECT balance FROM accounts WHERE id = 1; # 仍是 300:REPEATABLE READ 走同一快照
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; # 500:当前读,并持有行锁
COMMIT;PostgreSQL 对照
PostgreSQL 对照
BEGIN; -- 默认 READ COMMITTED
SELECT balance FROM accounts WHERE id = 1; -- 300
-- 另一会话提交 500 之后
SELECT balance FROM accounts WHERE id = 1; -- 500,每条语句取新快照
COMMIT;
BEGIN ISOLATION LEVEL REPEATABLE READ; -- 需要事务级一致性视图时显式指定
SELECT balance FROM accounts WHERE id = 1;
COMMIT;两库快照行为对照
| 对比项 | MySQL InnoDB | PostgreSQL |
|---|---|---|
| 默认隔离级别 | REPEATABLE READ | READ COMMITTED |
| 快照建立时机 | 事务首次读时 | RC 每条语句、RR 事务首条语句 |
| 旧版本位置 | undo log 回滚段 | 堆表死元组,由 VACUUM 回收 |
| 超期版本后果 | undo 表空间膨胀 | 表与索引膨胀、查询变慢 |
| 当前读写法 | SELECT … FOR UPDATE / LOCK IN SHARE MODE | SELECT … FOR UPDATE / FOR SHARE |
长事务是 MVCC 的头号问题:InnoDB 的 undo 因它无法截断,PG 的死元组无法回收,最终表现为磁盘膨胀与查询逐步变慢。应用层应设置事务超时并监控最老活动事务。
快照读不阻塞写、写也不阻塞快照读,但两个事务更新同一行仍会互相等待;「读不加锁」不等于没有并发问题,更新丢失要靠乐观锁或当前读解决。