首页/数据/SQL 速查/MVCC 与快照读:为什么两次查询结果不同
变更与事务52 条

MVCC 与快照读:为什么两次查询结果不同

多版本并发控制让读不阻塞写:PostgreSQL 每个语句取快照(READ COMMITTED)、REPEATABLE READ 取事务快照;InnoDB 通过 undo 构造一致性读视图。

SQL 速查数据更新于 2026-10-13

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 InnoDBPostgreSQL
默认隔离级别REPEATABLE READREAD COMMITTED
快照建立时机事务首次读时RC 每条语句、RR 事务首条语句
旧版本位置undo log 回滚段堆表死元组,由 VACUUM 回收
超期版本后果undo 表空间膨胀表与索引膨胀、查询变慢
当前读写法SELECT … FOR UPDATE / LOCK IN SHARE MODESELECT … FOR UPDATE / FOR SHARE
长事务是 MVCC 的头号问题:InnoDB 的 undo 因它无法截断,PG 的死元组无法回收,最终表现为磁盘膨胀与查询逐步变慢。应用层应设置事务超时并监控最老活动事务。
快照读不阻塞写、写也不阻塞快照读,但两个事务更新同一行仍会互相等待;「读不加锁」不等于没有并发问题,更新丢失要靠乐观锁或当前读解决。

易错点

  • 在 READ COMMITTED 下写「读—判断—写」的业务逻辑,两次查询之间数据已变,判断失效
  • 长事务不提交导致 undo 或死元组堆积,磁盘爆满后才发现是应用没关事务
  • 以为 REPEATABLE READ 能防更新丢失:基于快照里的旧值更新仍会覆盖他人提交
  • 滥用 FOR UPDATE 做快照读,加锁范围远超预期,引发大面积锁竞争

数据来源与参考

  • PostgreSQL Documentation · Concurrency Control
  • MySQL 8.0 Reference Manual · InnoDB Multi-Versioning
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌