数据库基础体系 · 第 28/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Oracle 事务与 Undo:读一致性、SCN、锁、Redo 和恢复机制

在 Oracle 中,“事务成功提交”并不等于“所有数据块已经写入数据文件”,而“查询没有被写操作阻塞”也不等于“查询看到了最新版本”。

这些现象分别由几组机制共同决定:

  • 事务定义一组具有原子性的数据库操作;
  • SCN为数据库事件提供逻辑时间;
  • Undo保存修改前的信息,用于回滚和构造一致性读;
  • 协调并发修改,保护事务之间的写入关系;
  • Redo记录需要持久化和重放的变化;
  • 恢复机制在实例故障或介质故障后,将数据库带回一个事务一致状态。

理解这些机制的关键,是区分三个问题:

  1. 查询应该看到哪个版本的数据?
  2. 并发事务能否同时修改同一数据?
  3. 提交后的修改在故障后如何保证不丢失?

Undo、锁和 Redo 分别主要回答这三个问题,但它们并不是互相独立的组件。


一、先建立事务模型

1.1 事务是什么

事务是一组逻辑上不可分割的数据库操作。事务通常具有 ACID 属性:

  • Atomicity,原子性:事务中的操作要么全部生效,要么全部撤销;
  • Consistency,一致性:事务提交后,数据应满足数据库约束和应用定义的不变量;
  • Isolation,隔离性:并发事务之间按照数据库隔离语义观察彼此的变化;
  • Durability,持久性:提交成功后,即使发生实例故障,提交结果也应能够恢复。

Oracle 的原子性不是通过“每次修改都立刻写入数据文件”实现的,而是依靠:

  • 数据块中的修改;
  • Undo 中的旧版本信息;
  • Redo 中的变化记录;
  • 提交状态;
  • 故障恢复时的重做和回滚。

1.2 事务何时开始和结束

在 Oracle 中,事务通常在执行第一条 DML 语句时开始:

INSERT
UPDATE
DELETE
MERGE

显式执行 COMMITROLLBACK 会结束当前事务。

例如:

UPDATE accounts
   SET balance = balance - 100
 WHERE account_id = 1;

UPDATE accounts
   SET balance = balance + 100
 WHERE account_id = 2;

COMMIT;

这两条更新属于同一个事务。若第二条语句失败,应用可以回滚整个事务,也可以使用保存点回滚部分操作:

SAVEPOINT after_debit;

UPDATE accounts
   SET balance = balance + 100
 WHERE account_id = 2;

ROLLBACK TO after_debit;

ROLLBACK TO after_debit 会撤销保存点之后的修改,但不会结束整个事务;保存点之前的修改仍然存在,之后仍可继续执行 SQL。

需要特别注意:

  • 一条 SQL 语句具有语句级原子性;
  • 应用异常不会自动替代事务设计;
  • 客户端的 autocommit 配置会改变事务边界;
  • Oracle 中执行 DDL 通常会隐式提交当前事务,且 DDL 前后都有提交语义影响,因此不能把 DDL 混入需要原子提交的 DML 事务中。

二、SCN:Oracle 的逻辑时间

2.1 SCN 的定义

SCN(System Change Number)是 Oracle 使用的逻辑时间标记。它不是操作系统时间,也不保证与秒、毫秒一一对应。

SCN 用于标识数据库变化的先后关系,例如:

  • 某条查询从哪个一致性点读取;
  • 某个事务何时提交;
  • 恢复操作需要将数据库推进到哪个重做位置;
  • Flashback 查询需要观察哪个历史版本。

可以把 SCN 抽象为一个单调推进的逻辑序列:

SCN1<SCN2SCN_1 < SCN_2

表示数据库在逻辑上先经历了 SCN_1,再经历了 SCN_2。它表达的是数据库内部的顺序,而不是现实时间间隔。

2.2 SCN 与提交的关系

事务执行修改时,并不意味着修改立即具有一个对所有查询可见的最终版本。事务提交时,Oracle 为提交事件建立相应的提交状态和 SCN。其他事务只有在其读取规则允许的情况下,才会看到该提交版本。

因此,需要区分:

  • 修改发生的时间
  • 事务提交的时间
  • 查询建立一致性视图的时间
  • 查询实际访问某个数据块的时间

它们可以不同。

2.3 如何观察 SCN

在具有相应权限的环境中,可以查询数据库当前 SCN:

SELECT current_scn
FROM   v$database;

也可以使用 Oracle 提供的 Flashback 相关接口获取当前系统 SCN,例如:

SELECT dbms_flashback.get_system_change_number
FROM   dual;

实际可用性取决于数据库版本、权限和部署环境。

ORA_ROWSCN 不应简单理解为“该行最后一次提交的精确 SCN”。在没有启用行级依赖跟踪时,它通常可能是块级信息;即使启用了相关能力,也不能把它当作通用事务审计字段。


三、Undo:旧版本、回滚和读一致性的共同基础

3.1 Undo 保存什么

当事务修改数据时,Oracle 会生成 Undo 信息。Undo 不是一份面向用户的完整历史表,而是用于撤销修改和构造旧版本所需的信息。

抽象地说,假设某行原来是:

balance = 1000

事务将其改为:

balance = 900

Undo 中会保留足以恢复旧状态的信息,例如“把 balance 恢复为 1000”以及相关事务和数据块定位信息。

Undo 通常位于 Undo 表空间的数据文件中。事务修改数据时,Oracle 同时会:

  1. 在 Undo 段中分配空间;
  2. 记录修改前的信息;
  3. 修改数据块的内存副本;
  4. 生成相应 Redo。

Undo 记录不等于“数据库的永久历史版本”。当没有活跃查询或恢复操作再需要某段 Undo 后,这些空间可以被重用。

3.2 Undo 的两个核心用途

用途一:事务回滚

执行:

UPDATE accounts
   SET balance = balance - 100
 WHERE account_id = 1;

ROLLBACK;

Oracle 使用 Undo 将数据恢复到事务开始前的状态。

如果事务因进程异常退出而未提交,实例恢复阶段也需要使用 Undo 撤销这些未提交修改。

用途二:构造一致性读

如果查询需要看到数据块在过去某个 SCN 的状态,而当前数据块已经被其他事务修改,Oracle 可以:

  1. 读取当前数据块;
  2. 检查该数据块或行的事务信息;
  3. 判断该修改在查询的一致性 SCN 上是否可见;
  4. 若不可见,沿 Undo 链应用旧版本信息;
  5. 得到查询所需的历史版本。

这就是 Oracle 多版本读一致性的核心。


四、读一致性:查询为什么不一定看到最新数据

4.1 读一致性的形式化条件

设一条查询在时刻建立一致性 SCN:

SqS_q

某行最近一次修改由事务 TT 完成,其提交 SCN 为:

ScS_c

在简化模型下:

  • TT 在查询一致性点之前已提交,即

ScSqS_c \leq S_q

则该修改对查询可见;

  • TT 在查询一致性点之后才提交,即

Sc>SqS_c > S_q

则该修改对该查询不可见,Oracle 需要使用 Undo 还原到更早版本;

  • 若事务在查询时仍未提交,则其修改对其他事务不可见。

这里的“可见”还受到查询谓词、事务隔离级别和其他数据库规则影响,但这个模型足以解释最基本的读一致性。

4.2 READ COMMITTED:语句级一致性

Oracle 默认隔离级别是 READ COMMITTED。其重要特征是:

每条 SQL 语句在自己的读取一致性点上执行。

不是整个连接,也不是整个事务只使用一个 SCN。

下面使用两个会话演示。

先准备测试表:

CREATE TABLE demo_accounts (
    account_id NUMBER PRIMARY KEY,
    balance    NUMBER NOT NULL
);

INSERT INTO demo_accounts(account_id, balance)
VALUES (1, 1000);

COMMIT;

会话 A

UPDATE demo_accounts
   SET balance = 900
 WHERE account_id = 1;

此时会话 A 尚未提交。

会话 B

SELECT balance
FROM   demo_accounts
WHERE  account_id = 1;

预期结果:

BALANCE
-------
1000

原因是会话 A 的修改尚未提交,会话 B 不能看到未提交版本。会话 B 不需要等待会话 A 的普通 UPDATE 完成,因为普通查询使用一致性读,并可以通过 Undo 读取旧版本。

会话 A 提交

COMMIT;

会话 B 再次执行查询

SELECT balance
FROM   demo_accounts
WHERE  account_id = 1;

预期结果:

BALANCE
-------
900

这是第二条 SQL 语句,它建立了新的读取一致性点,因此可以看到会话 A 已提交的修改。

4.3 同一条查询内部的一致性

如果一条查询需要读取多个数据块,执行过程中其他事务可能正在提交修改。Oracle 不会让同一条普通查询随意混合不同时间点的数据,而是尽可能依据该语句的一致性 SCN 构造结果。

例如:

SELECT SUM(balance)
FROM   demo_accounts;

它应当基于该语句的一致性视图计算,而不是对每一行分别使用不同的“当前最新版本”。

这也是为什么长时间运行的查询可能需要大量 Undo:查询越早开始,持续时间越长,越可能需要沿 Undo 链恢复较旧版本。

4.4 典型失败:ORA-01555

如果查询需要的旧版本已经被 Undo 空间重用,可能出现:

ORA-01555: snapshot too old

其逻辑过程可以表示为:

  1. 查询在 SCN SqS_q 建立一致性视图;
  2. 查询访问某个块时,发现该块已被修改;
  3. 为了还原到 SqS_q,需要访问 Undo;
  4. 需要的 Undo 已被后续事务覆盖;
  5. Oracle 无法构造查询所需的历史版本;
  6. 查询失败。

这不只是“Undo 表空间太小”一种原因。常见诱因包括:

  • 长时间运行的查询;
  • 大量 DML 快速重用 Undo;
  • 查询通过游标长时间分批获取;
  • 应用在一次逻辑读取过程中频繁提交;
  • Undo 保留时间不足;
  • 某些块访问和清理场景增加了对旧版本的需求。

UNDO_RETENTION 是保留时间目标,而不是绝对保证。若 Undo 表空间没有启用 retention guarantee,空间压力下仍可能重用旧 Undo。启用保证后,可能转而导致 DML 因没有可用 Undo 空间而失败,因此不能把它当成无代价开关。


五、事务隔离级别与一致性读的边界

5.1 READ COMMITTED

Oracle 默认的 READ COMMITTED 能避免脏读:

  • 看不到其他事务未提交的数据;
  • 每条语句看到一个一致性视图;
  • 同一事务中的两条查询可能看到不同结果,因为中间可能有其他事务提交。

例如:

-- 事务中第一次查询
SELECT balance FROM demo_accounts WHERE account_id = 1;

-- 其他事务提交修改后
-- 同一事务中第二次查询
SELECT balance FROM demo_accounts WHERE account_id = 1;

两次结果可能不同,这是语句级读一致性的正常表现。

5.2 SERIALIZABLE

可显式设置:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

在 Oracle 的 SERIALIZABLE 语义下,事务中的查询基于事务开始时建立的逻辑一致性视图;如果事务尝试修改一个在事务开始后被其他事务修改并提交的数据,可能失败:

ORA-08177: can't serialize access for this transaction

这不是普通锁等待,而是可串行化执行条件无法满足。应用通常需要捕获该错误并重新开始整个事务,而不是只重复失败的单条语句。

SERIALIZABLE 不意味着数据库为每个读取的行都加锁。它主要通过一致性视图和冲突检测实现隔离。

5.3 READ ONLY 事务

只读事务可以要求事务期间不执行 DML,同时保持一致性视图:

SET TRANSACTION READ ONLY;

SELECT ...

它适用于需要在同一个事务中观察稳定数据集的场景,但并不意味着底层数据不会变化;其他事务仍可修改和提交,当前事务只是继续按照自己的读一致性规则读取。


六、锁:协调并发写入,而不是制造所有读取等待

6.1 Oracle 的多版本读与锁的关系

Oracle 使用多版本并发控制。普通查询通常读取一致性版本,因此:

  • 读通常不阻塞写;
  • 写通常不阻塞普通读;
  • 写与写之间需要协调;
  • 需要锁定当前版本的操作可能等待其他事务。

“Oracle 查询不加锁”是过度简化的说法。更准确的说法是:

普通一致性查询通常不对被读取的数据行施加阻止并发 DML 的行锁;但特定查询、数据字典操作和当前读仍可能涉及锁或等待。

6.2 行级并发修改

假设初始余额为 1000。

会话 A

UPDATE demo_accounts
   SET balance = balance - 100
 WHERE account_id = 1;

会话 A 获得与该事务和行修改相关的锁,但尚未提交。

会话 B

UPDATE demo_accounts
   SET balance = balance - 50
 WHERE account_id = 1;

会话 B 通常会等待,而不是直接把余额更新成某个不确定结果。

原因是会话 B 要修改同一行的当前版本,而会话 A 尚未结束。会话 B 必须等待会话 A:

  • 提交:然后基于新的已提交状态继续判断和执行;
  • 回滚:然后基于恢复后的状态继续执行;
  • 或发生错误、超时、死锁等。

若会话 A 执行:

COMMIT;

会话 B 才可能继续。最终结果通常是先扣 100,再扣 50,即 850,而不是简单的“最后写入覆盖”。

6.3 TX、TM 和行锁的概念边界

Oracle 内部锁信息较复杂,但理解并发诊断时可以先区分:

  • TX enqueue:与事务和行级并发修改相关;
  • TM enqueue:与表级 DML 锁相关,用于协调表上的 DML 与某些结构性操作;
  • 行级锁信息:通常记录在数据块行头和 ITL 等结构中,并通过事务信息关联到持有者。

Oracle 的行级锁不是为表中每一行维护一个独立的、庞大的内存锁对象。数据块中的行锁信息与事务状态共同支持并发控制。

表级 DML 锁也不等于“锁住整张表上的所有行”。例如普通 UPDATE 会获得相应的表级 DML 锁模式和行级锁;其他事务仍可以更新不冲突的行。

6.4 SELECT FOR UPDATE 是当前读

普通 SELECT 主要是一致性读,而:

SELECT account_id, balance
FROM   demo_accounts
WHERE  account_id = 1
FOR UPDATE;

用于读取并锁定当前行,常见于“先读取,再由当前事务修改”的流程。

它可能等待已有事务释放行锁。可以使用:

SELECT account_id, balance
FROM   demo_accounts
WHERE  account_id = 1
FOR UPDATE NOWAIT;

若目标行被其他事务锁定,语句立即报错,而不是等待。

也可以使用:

SELECT account_id, balance
FROM   demo_accounts
WHERE  account_id = 1
FOR UPDATE SKIP LOCKED;

这会跳过当前已被锁定的行,常用于并发任务领取队列,但它改变了结果集语义:返回的不是“所有符合条件的行”,而是“当前未被跳过的可锁定行”。

6.5 锁等待、超时与死锁

典型现象:

  • 等待其他事务提交或回滚;
  • 使用 NOWAIT 时立即失败;
  • 配置了等待时间时,可能收到锁超时错误;
  • 两个事务交叉持有资源时,可能发生死锁。

死锁示例:

事务 A:锁住行 1,然后等待行 2
事务 B:锁住行 2,然后等待行 1

Oracle 会检测这种环路,通常让其中一个事务收到:

ORA-00060: deadlock detected while waiting for resource

应用必须处理该异常并回滚适当范围的事务。仅仅“再次执行同一条 SQL”可能留下错误的事务状态。

诊断锁等待时,可以结合动态性能视图查看会话、阻塞者和等待事件,例如:

SELECT sid,
       serial#,
       username,
       event,
       blocking_session,
       seconds_in_wait
FROM   v$session
WHERE  blocking_session IS NOT NULL;

具体字段和所需权限取决于 Oracle 版本与权限配置。诊断时还应找到阻塞事务为什么没有提交,而不是只终止等待者。


七、Redo:提交持久性和恢复的记录

7.1 Redo 与 Undo 的区别

可以用下表建立基本边界:

机制 主要内容 主要用途
Undo 修改前的信息及事务相关信息 回滚、构造一致性读、实例恢复中的事务回滚
Redo 数据库变化的重做记录 提交持久性、实例恢复、介质恢复、日志传输
并发控制状态 防止冲突写入、协调事务

Undo 与 Redo 不是互相替代的:

  • 有 Undo,不代表提交后数据块已经持久化;
  • 有 Redo,不代表查询可以直接读到旧版本;
  • 锁只能控制并发,不能代替故障恢复。

7.2 DML 为什么同时产生 Undo 和 Redo

执行 DML 时,Oracle 不仅修改用户表数据块,也会修改 Undo 段中的块。于是至少有两类变化:

  1. 用户数据块发生变化;
  2. Undo 块发生变化。

这些变化都需要进入 Redo,以便在故障后重做。因此“一条更新只生成数据变化的 Redo”是不准确的;Undo 本身的变化也会受到 Redo 保护。

7.3 Write-Ahead Logging 与提交

Oracle 遵循日志先行的基本原则:

在确认事务提交成功前,相关 Redo 必须先被写入持久化的联机重做日志。

典型提交路径可以抽象为:

  1. 事务修改数据块;
  2. 数据块在 SGA 的 Buffer Cache 中变化;
  3. 同时生成 Redo,进入内存中的 Redo 缓冲区;
  4. 执行 COMMIT
  5. LGWR 将必要的 Redo 写入联机重做日志;
  6. 写日志成功后,提交可以向客户端确认;
  7. 被修改的数据块可以稍后由 DBWn 写入数据文件。

因此,提交并不要求 DBWn 在返回前把所有数据块写入数据文件。提交可靠性主要依靠 Redo,而不是依靠数据文件当时已经包含最终数据。

7.4 联机 Redo 与归档 Redo

  • 联机重做日志用于近期变化记录,并以日志组和成员等形式循环使用;
  • 归档重做日志是联机日志切换后保存下来的历史 Redo,用于介质恢复、备份恢复、Data Guard 等。

Redo 记录不是普通 SQL 文本。恢复时,Oracle 根据内部变化记录重放数据库块变化,而不是重新执行用户原始 SQL。


八、从事务提交到数据文件:SGA、后台进程与数据流

这一过程需要结合 Oracle 数据库架构理解。

8.1 Buffer Cache 中的数据块

客户端进程执行 DML 时,服务器进程通常在 SGA 的 Buffer Cache 中读取和修改数据块。修改后的块称为 dirty buffer,不一定立即写入数据文件。

Undo 块也会经过相应的缓存和写出路径。

8.2 Redo 生成和 LGWR

Redo 先进入 SGA 中的 Redo 缓冲区。LGWR 在以下情形之一发生时写入联机 Redo:

  • 事务提交;
  • Redo 缓冲区达到一定条件;
  • 定期或其他内部条件触发;
  • 数据库需要推进相关恢复位置。

提交时,服务器进程需要等待必要的日志写入完成。这个等待在性能诊断中通常表现为与日志同步相关的等待事件。

8.3 DBWn 写数据块

DBWn 负责把脏数据块写入数据文件,但其写出时间与事务提交时间可以不同。这样做可以:

  • 减少每次提交都写大量数据块的成本;
  • 通过 Redo 在故障后重建已提交变化;
  • 按检查点和缓冲区压力组织写出。

8.4 检查点

检查点推进数据库恢复所需的边界,并促使部分脏块写入数据文件。检查点并不等于“数据库已经完全备份”,也不等于“所有事务都已提交”。

检查点的一个重要作用是减少实例恢复时需要扫描和重做的日志范围,但具体恢复工作仍需结合数据文件头、控制文件、联机 Redo 和事务状态判断。


九、回滚并不等于恢复:两条不同路径

“Rollback”与“Recovery”经常被混用,但它们处理的问题不同。

9.1 用户主动回滚

事务仍在运行时:

ROLLBACK;

Oracle 使用该事务的 Undo 撤销未提交修改,并释放相关资源。

9.2 语句失败时的回滚

一条 SQL 通常具有语句级原子性。若语句执行中发生错误,Oracle 会撤销该语句已经完成的部分操作,但不必然自动回滚整个事务。

例如:

UPDATE demo_accounts
   SET balance = balance - 100
 WHERE account_id = 1;

若后续另一条 SQL 失败,前一条更新可能仍处于当前事务中,除非应用显式执行:

ROLLBACK;

或客户端框架根据异常策略进行回滚。

9.3 实例崩溃后的恢复

假设数据库实例突然断电:

  • 某个已提交事务的修改只在 Buffer Cache 中;
  • 相应 Redo 已经写入联机日志;
  • 某个未提交事务的部分修改也可能已经写入数据文件。

重启时,实例恢复大致需要:

  1. 从检查点相关位置开始读取 Redo;
  2. 将已记录的变化重做到数据块;
  3. 找出恢复后仍未提交的事务;
  4. 使用 Undo 撤销这些未提交事务;
  5. 使数据库回到事务一致状态。

这说明:

  • 已提交但尚未写入数据文件的数据,可以通过 Redo 找回来;
  • 已写入数据文件但未提交的数据,可以通过 Undo 撤销;
  • 恢复不是简单地“把所有日志重新执行一遍”。

现代 Oracle 中,实例恢复由数据库实例的恢复机制执行,不能简单归因于某个单独后台进程。SMON 与恢复相关,但不应把所有恢复职责笼统地归于 SMON;PMON 主要负责进程清理和相关资源处理,也不是提交持久性的主体。


十、介质恢复:数据文件损坏时如何使用 RMAN 和 Redo

实例恢复假设联机 Redo 和控制信息仍然可用。若数据文件损坏、磁盘丢失或需要恢复到备份,则进入介质恢复范围。

10.1 介质恢复的基本数据流

一个典型的 RMAN 恢复过程是:

  1. 从备份中恢复数据文件;
  2. 使用归档 Redo 和必要的联机 Redo;
  3. 将变化重做到目标 SCN、时间点或日志位置;
  4. 处理恢复过程中的事务一致性;
  5. 打开数据库,或以指定方式完成恢复。

RMAN 示例:

CONNECT TARGET /

STARTUP MOUNT;

RESTORE DATABASE;
RECOVER DATABASE;

ALTER DATABASE OPEN;

这只是完整恢复场景的骨架。实际环境还涉及:

  • 控制文件是否可用;
  • 是否需要恢复控制文件;
  • 备份集和归档日志是否完整;
  • 是否使用 catalog;
  • 是否执行不完全恢复;
  • 是否存在只读表空间、加密、TDE 或跨平台因素;
  • 数据库是否处于 ARCHIVELOG 模式。

不要把 RECOVER DATABASE 理解为“从 Undo 恢复数据”。它主要利用 Redo 重建备份之后发生的变化;Undo 在事务一致性和未提交事务回滚中仍然发挥作用。

10.2 Redo 不是备份

Redo 只能记录从某个已有数据库状态开始发生的变化。若数据文件、控制文件和所需日志都丢失,仅凭 Redo 不能凭空构造完整数据库。

因此:

  • 数据文件备份提供恢复起点;
  • Redo 提供备份之后的变化;
  • 控制文件、参数文件、密码文件和归档日志共同构成可恢复环境的一部分。

十一、Data Guard 与 RAC:Undo、Redo 和锁的部署边界

11.1 Data Guard 的边界

Data Guard 主要通过传输和应用 Redo,使物理备用数据库保持与主库的变化同步。

其核心关系是:

主库事务修改
    ↓
主库生成 Redo
    ↓
Redo 传输到备用库
    ↓
备用库接收并应用 Redo
    ↓
备用数据库推进到相应恢复位置

Data Guard 传输的是 Redo,而不是把主库当前 Undo 表空间逐条复制成备用库的查询版本。

因此需要区分:

  • 主库本地查询的一致性读依赖主库的 Undo;
  • 备用库应用变化依赖接收和应用 Redo;
  • 备用库上的只读查询是否能看到所需历史版本,还取决于备用库自身的 Undo、查询持续时间和相关配置。

Data Guard 的保护模式还涉及提交时日志传输确认边界。同步、异步传输等配置会影响主库在提交时等待备用库确认的范围,但不改变 Undo 用于本地回滚和一致性读的基本职责。

11.2 RAC 的边界

RAC 中多个实例访问同一组共享数据库文件。每个实例有自己的 SGA、后台进程和 Redo 线程;实例之间通过集群机制协调全局缓存和锁资源。

在 RAC 中:

  • 不同实例上的事务仍使用 Undo;
  • 行级并发冲突需要跨实例协调;
  • 数据块可能需要通过全局缓存服务在实例之间传递;
  • Redo 通常按实例线程产生;
  • 一个实例故障后,其他实例需要参与实例恢复和事务清理。

因此,单实例中“本地缓存、锁等待、Redo 写入”的时间关系,在 RAC 中还要加上跨实例消息和全局缓存访问成本。但基本语义不变:

  • 未提交修改不能被其他事务普通读取;
  • 冲突写入必须协调;
  • 提交持久性依赖 Redo;
  • 故障后需要重做已记录变化并回滚未完成事务。

十二、一个完整并发算例

考虑账户转账:

UPDATE accounts
   SET balance = balance - 100
 WHERE account_id = 1;

UPDATE accounts
   SET balance = balance + 100
 WHERE account_id = 2;

COMMIT;

假设事务开始前:

账户 1:1000
账户 2:500
总额:1500

步骤一:执行第一条更新

事务修改账户 1:

当前数据块中的账户 1:900
Undo:账户 1 的旧值为 1000
Redo:记录数据块和 Undo 块的变化
事务状态:未提交

其他会话的普通查询仍可能读到:

账户 1:1000

因为 900 版本未提交,查询通过 Undo 得到一致性旧版本。

步骤二:执行第二条更新

事务修改账户 2:

当前数据块中的账户 2:600
Undo:账户 2 的旧值为 500
Redo:继续记录相关变化
事务状态:未提交

其他会话仍不能把 900 和 600 作为已提交的转账结果读取。

步骤三:提交

执行:

COMMIT;

Oracle 确保相关 Redo 达到提交持久性要求,然后记录事务已提交的状态。

此时:

已提交结果:
账户 1:900
账户 2:600
总额:1500

数据块可能仍有一部分尚未写入数据文件,但如果随后发生实例故障,恢复可以使用 Redo 重做这些变化。

步骤四:事务中途失败

如果第二条更新之后进程崩溃,但事务没有提交:

账户 1 的 900 可能已经进入数据文件
账户 2 的 600 可能仍在缓存中
事务状态:未提交

实例恢复会:

  1. 根据 Redo 重做必要变化;
  2. 识别该事务未提交;
  3. 根据 Undo 把账户 1 恢复为 1000;
  4. 把账户 2 恢复为 500;
  5. 最终不留下半笔转账。

这正是 Undo 与 Redo 配合实现原子性和持久性的过程。


十三、常见误解与失败表现

13.1 “提交后数据一定已经写入数据文件”

不准确。

提交确认的关键是必要 Redo 已持久化。数据块可以稍后由 DBWn 写入数据文件。故障后通过 Redo 恢复已提交变化。

13.2 “Undo 只是为了 ROLLBACK”

不准确。

Undo 同时用于:

  • 用户回滚;
  • 未提交事务的实例恢复;
  • 普通查询构造过去 SCN 的一致性版本;
  • 某些 Flashback 查询和相关功能。

13.3 “SELECT 永远不会等待”

不准确。

普通一致性读通常不会因普通行级 DML 等待,但以下场景可能等待或失败:

  • SELECT FOR UPDATE
  • DDL 与 DML 的锁冲突;
  • 数据字典或结构操作;
  • 块访问、I/O、缓存、并行执行等非行锁原因;
  • 需要的 Undo 不存在,最终出现 ORA-01555

13.4 “READ COMMITTED 保证同一事务重复读取相同结果”

不准确。

Oracle 默认是语句级一致性。事务内两条查询之间,其他事务可以提交变化,因此两次查询可能不同。

13.5 “把 Undo 保留时间调得很大就不会 ORA-01555”

不准确。

Undo 保留受到空间和写入压力影响。若空间不足,Undo 可能被重用;若启用保证保留,又可能导致 DML 因无法分配 Undo 而失败。还应诊断长查询、游标生命周期和应用提交模式。

13.6 “Redo 可以替代 RMAN 备份”

不准确。

Redo 记录变化,不提供完整数据库基线。介质恢复通常需要备份数据文件,再应用归档和联机 Redo。

13.7 “杀掉阻塞会话就完成了回滚”

不一定。

终止会话后,未提交事务仍需要清理和回滚;大型事务的回滚可能持续一段时间。在此期间,相关资源未必立即全部可用。生产处理前应确认事务大小、业务影响和阻塞关系。


十四、如何沿着现象定位机制

遇到事务问题时,可以先把现象归类。

14.1 查询看到旧值

先判断:

  1. 查询是否在其他事务提交前开始;
  2. 当前是否是同一条长时间运行的语句;
  3. 是否处于 READ COMMITTEDSERIALIZABLE
  4. 旧版本是否由 Undo 构造;
  5. 查询是否运行在 Data Guard 备用库或其他副本上。

“旧值”可能是正常的一致性读,不一定是数据延迟或错误。

14.2 DML 长时间等待

重点检查:

  • 阻塞会话;
  • 阻塞者当前事务是否提交;
  • 是否更新了同一行;
  • 是否存在 SELECT FOR UPDATE
  • 是否发生 DDL 与 DML 冲突;
  • 是否是 RAC 中的全局缓存或跨实例等待。

不要只看等待者的 SQL,也要查看阻塞者为何长期不结束事务。

14.3 出现 ORA-01555

应结合:

  • 查询开始时间和持续时间;
  • Undo 表空间大小与使用情况;
  • Undo 保留目标;
  • DML 峰值;
  • 游标是否跨越大量提交;
  • 是否存在异常长事务或批量修改。

简单增加 Undo 可能有效,但只有当根因确实是历史版本空间不足时才成立。

14.4 提交很慢

提交路径通常与 Redo 写入相关。应检查:

  • 日志同步等待;
  • 联机 Redo 日志 I/O;
  • 存储延迟;
  • 日志组切换;
  • 过高的提交频率;
  • RAC 中的日志和全局协调成本。

COMMIT 随意移除会扩大事务范围和锁持有时间,不能把所有提交慢问题都用“少提交”解决。

14.5 故障后恢复时间很长

恢复时间受到多方面影响:

  • 检查点位置;
  • 需要扫描和应用的 Redo 范围;
  • 未提交事务的回滚量;
  • 数据文件和日志 I/O;
  • RAC 实例故障后的全局恢复;
  • 介质恢复需要应用的归档日志数量。

因此恢复性能不是只由“Undo 大小”或“Redo 大小”单独决定的。


十五、机制之间的最终关系

可以用一条事务生命周期概括:

开始事务
   ↓
修改数据块
   ├─ 生成 Undo:保存旧版本和回滚信息
   ├─ 生成 Redo:记录数据块与 Undo 块变化
   └─ 获取并维护并发锁
   ↓
执行查询
   ├─ 根据 SCN 判断版本可见性
   ├─ 需要时通过 Undo 构造旧版本
   └─ 冲突写入等待锁或报告并发错误
   ↓
COMMIT
   ├─ 建立提交状态和提交逻辑时间
   ├─ LGWR 持久化必要 Redo
   └─ 后续由 DBWn 等写入数据文件
   ↓
发生故障
   ├─ Redo 重做已记录变化
   └─ Undo 回滚未提交事务

其中:

  • SCN决定“从哪个逻辑时间观察”;
  • Undo决定“如何得到过去版本或撤销未提交修改”;
  • 决定“并发修改是否冲突以及是否等待”;
  • Redo决定“提交后的变化如何在故障后重建”;
  • 恢复把这些信息组合起来,使数据库重新达到事务一致状态。

只掌握其中一个术语,无法解释 Oracle 的完整行为。真正重要的是理解它们在同一条数据变化路径上的配合:查询依赖 SCN 和 Undo,写入依赖锁和 Undo,提交依赖 Redo,故障恢复则同时依赖 Redo、Undo、数据文件和控制信息。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。