Oracle Undo 表空间——读一致性、闪回查询与 ORA-01555 的三角博弈

Undo 是 Oracle 最被低估的机制之一。很多 DBA 只知道"Undo 用来回滚",不知道它还是读一致性的基石、闪回查询的粮仓、以及 ORA-01555 的罪魁祸首。我见过一个客户,Undo 表空间设了 500GB,还是报 ORA-01555,DBA 一脸懵:"空间这么大,怎么还不够?"因为 Undo 不够不是空间问题,是保留时间问题。这篇文章把 Undo 的三重身份拆清楚,再讲一个 500GB Undo 报 ORA-01555 的奇葩案例。

1 Undo 不是回滚专用,是读一致性的命根子

Undo 的第一重身份是事务回滚——这个大家都知道。事务执行 UPDATE 时,Oracle 把旧值写到 Undo,新值写到数据块。如果事务回滚,Oracle 从 Undo 里读旧值,写回数据块。但 Undo 的第二重身份更重要:读一致性(Read Consistency)。Oracle 的默认隔离级别是 READ COMMITTED,意思是查询只能看到查询开始那一刻已经提交的数据。如果查询执行期间,有其他事务修改了某行并提交,查询不能看到新值,必须看旧值。这个旧值从哪来?Undo。所以 Undo 不仅存"未提交事务"的旧值,还存"已提交但查询需要"的旧值。

Undo 的第三重身份是闪回查询(Flashback Query)。SELECT * FROM orders AS OF TIMESTAMP SYSDATE - 1/24;这条查询看的是一小时前的 orders 表,如果一小时内的变更已经被覆盖(新的 Undo 覆盖了旧的),闪回查询就报 ORA-01555。所以 Undo 表空间的大小,必须同时满足三个需求:回滚未提交事务、支持读一致性、支持闪回查询。任何一个不够,都会出问题。

2 Undo 段的生命周期:ACTIVE → UNEXPIRED → EXPIRED → FREE

Undo 表空间由多个 Undo Segment(回滚段)组成,比如 _SYSSMU1$ 到 _SYSSMU10$。每个 Segment 由多个 Extent 组成,Extent 是分配和回收的最小单位。Extent 的状态分四种:ACTIVE:里面有未提交事务,绝对不能覆盖。UNEXPIRED:里面的事务已提交,但还在 UNDO_RETENTION 保留期内,可能被读一致性查询或闪回查询用到,原则上不覆盖,但空间紧张时可以覆盖(报 ORA-01555)。EXPIRED:保留期已过,可以安全覆盖。FREE:已经被回收,可以分配给新事务。

Oracle 的 Undo 空间管理策略是循环覆盖:新事务需要 Undo 空间时,优先用 FREE 的 Extent;如果没有 FREE,就用 EXPIRED 的;如果 EXPIRED 也没有,就用 UNEXPIRED 的,但这时如果读一致性查询需要这个 Extent,查询就报 ORA-01555。如果 UNEXPIRED 也没有,就报 ORA-30036(无法扩展回滚段)。所以 ORA-01555 和 ORA-30036 是 Undo 不足的两个表现:前者是"有空间但保留期不够",后者是"彻底没空间了"。

3 ORA-01555 的四种场景,很多人只见过第一种

ORA-01555(snapshot too old)有四种触发场景:场景一,查询执行时间太长,超过了 UNDO_RETENTION。比如一个报表查询跑了 2 小时,UNDO_RETENTION 是 900 秒(15 分钟),查询需要的 Undo 在 15 分钟后被覆盖,报 ORA-01555。这是最常见的场景,解决办法是加大 UNDO_RETENTION 或优化查询。场景二,Fetch Across Commits。PL/SQL 游标打开后,循环 FETCH,循环体里做了 COMMIT。COMMIT 后,游标需要的读一致性 SCN 没变,但 Undo 可能被覆盖。这是 PL/SQL 的经典陷阱,解决办法是用 OPEN FOR 重新打开游标,或者把 COMMIT 移到循环外。场景三,延迟块清除(Delayed Block Cleanout)。事务提交后,数据块上的事务标志不会立即清除(为了性能),而是等下次访问该块时清除。如果查询访问了一个很久没动的块,发现上面的事务标志还在,就去 Undo 里查这个事务的状态。如果 Undo 已经被覆盖,就报 ORA-01555。这种场景最隐蔽,因为查询本身很快,但访问的块很老。场景四,闪回查询时间窗口超过 UNDO_RETENTION。AS OF TIMESTAMP SYSDATE - 2,但 UNDO_RETENTION 只有 1 小时,直接报 ORA-01555。

我们那个 500GB Undo 报 ORA-01555 的客户,就是场景三。他们的库是一个历史归档库,数据基本不动,但每天有个巡检脚本全表扫描所有表。扫描到某些十年没动的块时,触发延迟块清除,去 Undo 查旧事务状态,Undo 虽然 500GB,但保留期只设了 300 秒(5 分钟),因为 DBA 觉得"历史库没事务,Undo 不需要留太久"。结果延迟块清除需要的事务状态在 5 分钟后就被覆盖了,全表扫描跑到一半就报 ORA-01555。解决办法不是加 Undo 空间,而是加 UNDO_RETENTION 到 3600 秒,或者对老表做 ANALYZE 强制块清除。

4 实验:UNDO_RETENTION 与 ORA-01555 的边界

实验环境:Oracle 19c,64G 内存,Undo 表空间 50GB。测试表 old_orders,1000 万行,数据是 5 年前导入的,之后很少修改。模拟场景三(延迟块清除)的 ORA-01555。

-- 1. 构造环境:Undo 保留期设很短
ALTER SYSTEM SET UNDO_RETENTION = 300 SCOPE = BOTH;

-- 2. 确认表很久没分析(块上可能有旧事务标志)
SELECT last_analyzed FROM dba_tables WHERE table_name = 'OLD_ORDERS';
-- NULL(从来没分析过)

-- 3. 会话 A:执行长查询(全表扫描)
DECLARE
    cnt NUMBER := 0;
BEGIN
    FOR rec IN (SELECT * FROM old_orders) LOOP
        cnt := cnt + 1;
        IF MOD(cnt, 100000) = 0 THEN
            DBMS_OUTPUT.PUT_LINE('Scanned ' || cnt || ' rows at ' || TO_CHAR(SYSDATE, 'HH24:MI:SS'));
        END IF;
    END LOOP;
END;
/

-- 4. 会话 B:制造事务覆盖 Undo
BEGIN
    FOR i IN 1..100000 LOOP
        UPDATE old_orders SET amount = amount + 1 WHERE order_id = i;
        COMMIT;
    END LOOP;
END;
/

-- 5. 预期结果:会话 A 报 ORA-01555
-- ORA-01555: snapshot too old: rollback segment number 5 with name "_SYSSMU5$" too small

实验结果:会话 A 在扫描到约 380 万行时报 ORA-01555。因为会话 B 的大量小事务快速消耗 Undo,UNDO_RETENTION=300 秒意味着 5 分钟后旧 Undo 就可以被覆盖。而 old_orders 表上的旧事务标志需要查询 5 分钟前的 Undo 来确认状态,但那些 Undo 已经被会话 B 的新事务覆盖了。把 UNDO_RETENTION 改成 3600 秒后,同样的实验不再报错。但注意,3600 秒的保留期意味着 Undo 表空间需要更大,因为旧 Undo 要留 1 小时才能被覆盖。我们测了保留期 300 秒时,Undo 峰值占用 8GB;保留期 3600 秒时,峰值占用 45GB(接近表空间上限 50GB)。所以如果保留期设长,必须同步加大 Undo 表空间。

5 TUNED_UNDORETENTION:自动调优还是自动挖坑

11g 引入了 TUNED_UNDORETENTION,默认开启。它的逻辑是:Oracle 根据当前 Undo 表空间的使用率和历史查询模式,动态调整实际保留期。比如 UNDO_RETENTION 设了 900 秒,但 Undo 表空间很空,TUNED_UNDORETENTION 可能实际保留 3600 秒,支持更长的闪回查询。听起来很美好,但实际是个坑。当 Undo 表空间紧张时,TUNED_UNDORETENTION 会拼命拉长保留期——它认为"既然空间快满了,那更要保留旧 Undo,因为新事务可能很快需要闪回"。这完全是个反向操作:空间越满,保留期越长,空间越不够用。我们那个 500GB Undo 的客户,TUNED_UNDORETENTION 把实际保留期调到了 4 小时,Undo 空间在凌晨 ETL 时爆满,然后 TUNED 又继续拉长保留期,形成恶性循环。最后 DBA 被迫关掉 TUNED_UNDORETENTION,改成固定 1800 秒,问题才解决。

【踩坑笔记】生产环境建议关掉 TUNED_UNDORETENTION,改用固定 UNDO_RETENTION。固定值的好处是可预测:你可以根据峰值事务速率精确计算 Undo 表空间大小。公式:Undo 表空间大小 = 峰值事务速率(TPS)× 平均 Undo 块数/事务 × UNDO_RETENTION / (2 × 3600)。除以 2 是因为 Undo 可以循环覆盖,不需要全部保留。

6 总结:Undo 管理的铁三角

Undo 管理有三个互相制约的维度:空间(表空间大小)、时间(UNDO_RETENTION)、并发(事务速率)。加大空间可以支持更长保留期或更高并发;缩短保留期可以节省空间但增加 ORA-01555 风险;降低并发(比如优化批量操作)可以减少 Undo 消耗。没有银弹,只有权衡。我的建议是:第一,固定 UNDO_RETENTION,别信 TUNED 的自动调优;第二,Undo 表空间设成自动扩展(AUTOEXTEND),但 MAXSIZE 要有限制(比如 200GB),防止失控增长;第三,对历史表定期做 ANALYZE 或 DBMS_STATS,强制延迟块清除,减少场景三的 ORA-01555;第四,长查询(报表、ETL)尽量放在只读备库或低峰期,避免跟高并发事务抢 Undo。最后送一句话:Undo 是数据库的时间机器,但时间机器需要燃料——燃料就是空间和保留期。燃料不够,机器就抛锚。


请使用浏览器的分享功能分享到微信等