Oracle Undo、Redo 与 SCN 原理
Oracle 的事务、一致性读、回滚和崩溃恢复,都离不开三个核心机制:Undo、Redo、SCN。
如果只背:
text
Undo 用于回滚,Redo 用于恢复,SCN 是系统变化号。这远远不够。真正要理解的是:
- 一条 update 执行时 Undo 和 Redo 分别记录什么。
- 查询为什么能看到某个时间点的一致数据。
- Oracle 为什么不会读到未提交数据。
- 提交后数据块没写盘,为什么宕机还能恢复。
- 为什么会出现 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 内部递增的一致性时间戳。
它不是普通时间,但可以用来回答:
- 这个查询应该看到哪个时间点的数据。
- 这个事务提交到了哪个位置。
- 恢复应该恢复到哪个一致点。
- 主备同步到了哪个位置。
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 时刻的值"]假设:
- 查询 Q 在 SCN=100 开始。
- 事务 T 在 SCN=120 提交,把余额从 100 改成 80。
- 查询 Q 读到这行时发现当前版本太新。
- Oracle 使用 Undo 构造 SCN=100 时能看到的旧值 100。
这就是一致性读。
为什么不会脏读
脏读是读到了别的事务还没提交的数据。
Oracle 避免脏读的原因:
- 查询有自己的 SCN。
- 未提交修改没有提交 SCN。
- 如果当前块里的值对查询不可见,就通过 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: 旧版本找不到常见诱因:
- 查询运行太久。
- Undo 表空间太小。
- 大量更新导致 Undo 被快速覆盖。
- 提交过于频繁,旧版本保留时间不足。
解决方向:
- 优化长查询,减少运行时间。
- 增大 Undo 表空间。
- 调整 Undo 保留策略。
- 避免高峰期跑超长报表。
商业场景:支付状态更新
sql
update payment_order
set status = 'SUCCESS',
paid_at = sysdate
where pay_no = 'P202607050001'
and status = 'PAYING';背后过程:
- Oracle 找到目标行所在数据块。
- 生成 Undo,记录原状态
PAYING。 - 在 Buffer Cache 中把状态改为
SUCCESS。 - 生成 Redo,记录数据块变化。
- commit 时 LGWR 写 Redo。
- 返回支付更新成功。
- 数据块可以稍后由 DBWR 刷盘。
- 如果宕机,Redo 重做成功状态。
- 如果事务未提交就失败,Undo 回滚到
PAYING。
常见坑
| 坑 | 后果 | 正确理解 |
|---|---|---|
| 以为 Undo 只用于回滚 | 无法理解一致性读 | Undo 还用于构造查询旧版本 |
| 以为 commit 必须刷数据文件 | 不理解 Redo | commit 关键是 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。