在 PostgreSQL 中,读已提交(Read Committed)和可重复读(Repeatable Read)的区别?
在 PostgreSQL 中,读已提交(Read Committed) 和 可重复读(Repeatable Read) 是最常用的两种事务隔离级别。PostgreSQL 默认使用“读已提交”。
它们的核心区别在于事务能够看到哪个时间点的数据(即数据快照的创建时机),以及它们如何处理并发修改引发的冲突。
以下是详细的区别和对比:
1. 核心区别:可见性快照(Snapshot)的创建时机
这是理解两者差异的最关键点(基于 PostgreSQL 的 MVCC 机制):
- Read Committed(读已提交):
- 快照时机:在同一个事务中,每一条 SQL 语句执行前都会重新获取一次最新的快照。
- 结果:同一事务内的两条相同查询,如果在此期间有其他事务提交了修改,两次查询可能会看到不同的结果。
- Repeatable Read(可重复读):
- 快照时机:在事务中的第一条非控制类 SQL 语句执行时获取一次快照,并在整个事务期间保持不变。
- 结果:同一事务内的不管查询多少次,看到的数据总是一致的(就像是在事务开始时给整个数据库拍了一张照片)。
2. 读现象(Read Phenomena)对比
| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-repeatable Read) | 幻读 (Phantom Read) |
|---|---|---|---|
| Read Committed | ❌ 阻止 | ⚠️ 允许 | ⚠️ 允许 |
| Repeatable Read | ❌ 阻止 | ❌ 阻止 | ❌ 阻止 (PG特有) |
⚠️ 特别注意(PostgreSQL 的特性):
根据 ANSI SQL 标准,可重复读(Repeatable Read)是允许“幻读”的。但 PostgreSQL 的 MVCC 机制非常强大,它的 Repeatable Read 级别不仅阻止了不可重复读,实际上也阻止了幻读。
3. 具体场景演示
假设有一个账户表 accounts,Alice 初始余额为 100。
场景 A:数据读取(不可重复读的区别)
在 Read Committed 级别下:
- 事务A:查询 Alice 余额(得到 100)。
- 事务B:将 Alice 余额修改为 200 并 Commit。
- 事务A:再次查询 Alice 余额(得到 200)。-> 这就是不可重复读。
在 Repeatable Read 级别下:
- 事务A:查询 Alice 余额(得到 100)。
- 事务B:将 Alice 余额修改为 200 并 Commit。
- 事务A:再次查询 Alice 余额(依然得到 100)。-> 保证了可重复读。
场景 B:并发更新(更新冲突的区别)
这是开发者在写代码时必须注意的巨大差异!
假设 Alice 余额为 100,事务 A 和事务 B 同时想给 Alice 加 50 块钱。
在 Read Committed 级别下(默认行为):
- 事务A:准备更新 Alice(
UPDATE ... WHERE id=1),获取行锁。 - 事务B:也准备更新 Alice,因为锁被 A 占有,B 会阻塞等待。
- 事务A:Commit。
- 事务B:解除阻塞。B 会重新读取这行数据的最新状态(余额变成150了),然后基于新状态执行更新(150+50=200),最后 Commit。
结果:余额正确地变成了 200。
在 Repeatable Read 级别下(报错行为):
- 事务A:准备更新 Alice,获取行锁。
- 事务B:也准备更新 Alice,阻塞等待。
- 事务A:Commit。
- 事务B:解除阻塞。但是,由于 B 在 Repeatable Read 级别,它不能修改在它的快照建立之后被其他事务修改过的数据。此时 PostgreSQL 会直接报错并且回滚事务B:
ERROR: could not serialize access due to concurrent update(错误:由于并发更新,无法序列化访问)。
结果:事务 A 成功,事务 B 报错。
4. 总结与建议:应该选哪个?
选择 Read Committed(绝大多数情况下的首选):
- 适用场景: 普通的增删改查、高并发的 Web 应用。
- 优点: 很少会因为并发导致报错,不需要在代码层写复杂的“事务重试”逻辑,性能好。
- 缺点: 无法保证一个长事务内多次读取的数据绝对一致。
选择 Repeatable Read:
- 适用场景:
- 复杂的数据报表/数据导出: 你需要查询多张表,并且要求这些表的数据在逻辑上必须处于同一时刻(比如财务对账,不能出现查 A 表是早上的数据,查 B 表是中午更新后的数据)。
- 严格的业务逻辑计算: 事务中的后续判断严格依赖于前面的查询结果。
- 缺点与要求: 极其容易遇到
could not serialize access报错。如果你的应用使用这个级别,你的代码必须捕获这个异常,并具备重新执行整个事务的逻辑。