Skip to content

Oracle Undo、Redo 与 SCN 原理

Oracle 的事务、一致性读、回滚和崩溃恢复,都离不开三个核心机制:Undo、Redo、SCN

如果只背:

text
Undo 用于回滚,Redo 用于恢复,SCN 是系统变化号。

这远远不够。真正要理解的是:

  1. 一条 update 执行时 Undo 和 Redo 分别记录什么。
  2. 查询为什么能看到某个时间点的一致数据。
  3. Oracle 为什么不会读到未提交数据。
  4. 提交后数据块没写盘,为什么宕机还能恢复。
  5. 为什么会出现 snapshot too old。

总体关系

mermaid
flowchart TD
    A["业务修改数据"] --> B["生成 Undo 旧值"]
    A --> C["生成 Redo 变更记录"]
    B --> D["支持回滚"]
    B --> E["支持一致性读"]
    C --> F["支持崩溃恢复"]
    G["SCN"] --> E
    G --> F

一句话:

Undo 保存修改前的旧值,Redo 保存如何恢复修改,SCN 标识数据库的一致性时间点。

SCN 是什么

SCN 是 System Change Number,可以理解为 Oracle 内部递增的一致性时间戳。

它不是普通时间,但可以用来回答:

  1. 这个查询应该看到哪个时间点的数据。
  2. 这个事务提交到了哪个位置。
  3. 恢复应该恢复到哪个一致点。
  4. 主备同步到了哪个位置。
mermaid
flowchart TD
    A["事务提交"] --> B["分配提交 SCN"]
    B --> C["后续查询按 SCN 判断可见性"]
    B --> D["恢复按 SCN 找一致点"]

Undo 保存什么

假设账户余额从 100 改成 80:

sql
update account
set balance = 80
where id = 1;

Undo 里要能支持“把它改回去”,所以会保存类似:

text
这行原来的 balance 是 100。
如果事务回滚,把 balance 恢复为 100。

Undo 的作用不只是回滚,还能构造旧版本。

Redo 保存什么

Redo 关注的是数据块发生了什么变化,目的是宕机后可以重做这些变化。

简化理解:

text
某个数据块的某个位置发生了修改。
如果实例崩溃,恢复时可以根据 redo 把修改重新应用。

Redo 不是为了撤销,而是为了重做。

一次 update 的完整过程

mermaid
flowchart TD
    A["客户端执行 update"] --> B["找到目标数据块"]
    B --> C["在 Buffer Cache 中修改块"]
    C --> D["生成 Undo 记录"]
    C --> E["生成 Redo 记录"]
    D --> F["Undo 本身的变化也需要 Redo 保护"]
    E --> G["事务提交"]
    G --> H["Redo 写入日志文件"]
    H --> I["返回提交成功"]
    I --> J["脏块以后再写入数据文件"]

注意一个关键点:Undo 本身也是数据,也要靠 Redo 保护。否则宕机后如果需要回滚未提交事务,却找不到 Undo,就无法恢复一致状态。

一致性读怎么发生

Oracle 普通查询不会读未提交数据。查询开始时会确定一个查询 SCN。

mermaid
flowchart TD
    A["select 开始"] --> B["确定查询 SCN"]
    B --> C["读取数据块"]
    C --> D{"块中行版本是否晚于查询 SCN"}
    D -- "否" --> E["直接返回当前块中的值"]
    D -- "是" --> F["根据 Undo 构造旧版本"]
    F --> G["返回查询 SCN 时刻的值"]

假设:

  1. 查询 Q 在 SCN=100 开始。
  2. 事务 T 在 SCN=120 提交,把余额从 100 改成 80。
  3. 查询 Q 读到这行时发现当前版本太新。
  4. Oracle 使用 Undo 构造 SCN=100 时能看到的旧值 100。

这就是一致性读。

为什么不会脏读

脏读是读到了别的事务还没提交的数据。

Oracle 避免脏读的原因:

  1. 查询有自己的 SCN。
  2. 未提交修改没有提交 SCN。
  3. 如果当前块里的值对查询不可见,就通过 Undo 构造可见旧版本。

所以普通查询不需要等待更新事务提交,也不会读它未提交的新值。

提交后为什么脏块可以晚点刷盘

事务提交时,不一定把数据块立刻写入数据文件。真正必须保证的是 Redo 已可靠写入。

mermaid
flowchart TD
    A["事务修改 Buffer Cache 中的数据块"] --> B["生成 Redo"]
    B --> C["commit"]
    C --> D["LGWR 写 redo 到 redo log file"]
    D --> E["提交成功"]
    E --> F["DBWR 以后把脏块写入数据文件"]

为什么这样做?

方式问题
每次提交都刷数据块大量随机 IO,性能差
提交只写内存宕机丢数据
提交写 Redo顺序写日志,性能和可靠性兼顾

宕机恢复时,Oracle 根据 Redo 把已提交但未写入数据文件的变化重做回来。

崩溃恢复做什么

实例崩溃后,Oracle 重启需要恢复到一致状态。

mermaid
flowchart TD
    A["实例重启"] --> B["读取控制文件和数据文件"]
    B --> C["应用 Redo 重做已记录变化"]
    C --> D["识别未提交事务"]
    D --> E["使用 Undo 回滚未提交变化"]
    E --> F["数据库打开到一致状态"]

恢复可以简化成两类动作:

动作目的
Roll Forward用 Redo 重放变化
Roll Back用 Undo 撤销未提交事务

ORA-01555 snapshot too old

snapshot too old 常见原因是查询需要的旧版本已经无法从 Undo 中构造出来。

mermaid
sequenceDiagram
    participant Q as 长查询
    participant U as 更新事务
    participant UN as Undo
    Q->>Q: 以 SCN=100 开始查询
    U->>UN: 大量生成 Undo
    UN->>UN: 旧 Undo 空间被复用
    Q->>UN: 需要 SCN=100 的旧版本
    UN-->>Q: 旧版本找不到

常见诱因:

  1. 查询运行太久。
  2. Undo 表空间太小。
  3. 大量更新导致 Undo 被快速覆盖。
  4. 提交过于频繁,旧版本保留时间不足。

解决方向:

  1. 优化长查询,减少运行时间。
  2. 增大 Undo 表空间。
  3. 调整 Undo 保留策略。
  4. 避免高峰期跑超长报表。

商业场景:支付状态更新

sql
update payment_order
set status = 'SUCCESS',
    paid_at = sysdate
where pay_no = 'P202607050001'
  and status = 'PAYING';

背后过程:

  1. Oracle 找到目标行所在数据块。
  2. 生成 Undo,记录原状态 PAYING
  3. 在 Buffer Cache 中把状态改为 SUCCESS
  4. 生成 Redo,记录数据块变化。
  5. commit 时 LGWR 写 Redo。
  6. 返回支付更新成功。
  7. 数据块可以稍后由 DBWR 刷盘。
  8. 如果宕机,Redo 重做成功状态。
  9. 如果事务未提交就失败,Undo 回滚到 PAYING

常见坑

后果正确理解
以为 Undo 只用于回滚无法理解一致性读Undo 还用于构造查询旧版本
以为 commit 必须刷数据文件不理解 Redocommit 关键是 Redo 可靠写入
长报表随便跑可能 ORA-01555长查询依赖 Undo 保留
DML 不提交持锁阻塞其他事务事务要短,及时提交或回滚
只看 SQL 不看等待排查方向错误Oracle 性能要看执行计划和等待事件

面试标准回答

text
Oracle 通过 Undo、Redo 和 SCN 保证事务一致性。Undo 保存修改前的旧值,用于事务回滚和一致性读;Redo 记录数据块变化,用于崩溃恢复;SCN 是数据库内部的一致性时间点。普通查询开始时会确定查询 SCN,如果读到的数据版本晚于该 SCN,就通过 Undo 构造旧版本,所以不会脏读。事务提交时关键是 Redo 可靠写入,数据块可以稍后刷盘,宕机后通过 Redo 重做已提交变化,再通过 Undo 回滚未提交事务。长查询如果需要的 Undo 旧版本被覆盖,可能出现 snapshot too old。