基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

在 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 级别下:

  1. 事务A:查询 Alice 余额(得到 100)。
  2. 事务B:将 Alice 余额修改为 200 并 Commit
  3. 事务A:再次查询 Alice 余额(得到 200)。-> 这就是不可重复读。

在 Repeatable Read 级别下:

  1. 事务A:查询 Alice 余额(得到 100)。
  2. 事务B:将 Alice 余额修改为 200 并 Commit
  3. 事务A:再次查询 Alice 余额(依然得到 100)。-> 保证了可重复读。

场景 B:并发更新(更新冲突的区别)

这是开发者在写代码时必须注意的巨大差异!

假设 Alice 余额为 100,事务 A 和事务 B 同时想给 Alice 加 50 块钱。

在 Read Committed 级别下(默认行为):

  1. 事务A:准备更新 Alice(UPDATE ... WHERE id=1),获取行锁。
  2. 事务B:也准备更新 Alice,因为锁被 A 占有,B 会阻塞等待
  3. 事务A:Commit
  4. 事务B:解除阻塞。B 会重新读取这行数据的最新状态(余额变成150了),然后基于新状态执行更新(150+50=200),最后 Commit
    结果:余额正确地变成了 200。

在 Repeatable Read 级别下(报错行为):

  1. 事务A:准备更新 Alice,获取行锁。
  2. 事务B:也准备更新 Alice,阻塞等待
  3. 事务A:Commit
  4. 事务B:解除阻塞。但是,由于 B 在 Repeatable Read 级别,它不能修改在它的快照建立之后被其他事务修改过的数据。此时 PostgreSQL 会直接报错并且回滚事务B
    ERROR: could not serialize access due to concurrent update (错误:由于并发更新,无法序列化访问)。
    结果:事务 A 成功,事务 B 报错。

4. 总结与建议:应该选哪个?

选择 Read Committed(绝大多数情况下的首选):

  • 适用场景: 普通的增删改查、高并发的 Web 应用。
  • 优点: 很少会因为并发导致报错,不需要在代码层写复杂的“事务重试”逻辑,性能好。
  • 缺点: 无法保证一个长事务内多次读取的数据绝对一致。

选择 Repeatable Read:

  • 适用场景:
    1. 复杂的数据报表/数据导出: 你需要查询多张表,并且要求这些表的数据在逻辑上必须处于同一时刻(比如财务对账,不能出现查 A 表是早上的数据,查 B 表是中午更新后的数据)。
    2. 严格的业务逻辑计算: 事务中的后续判断严格依赖于前面的查询结果。
  • 缺点与要求: 极其容易遇到 could not serialize access 报错。如果你的应用使用这个级别,你的代码必须捕获这个异常,并具备重新执行整个事务的逻辑。
00:00
00:00