☰
Oracle redo与undo协同机制深度解析:从物理日志到事务恢复
2026/10/14 15:36:15 网站建设 项目流程

1. 为什么 DBA 第一次看懂 redo 和 undo 时,手里的咖啡都凉了

“redo 是重做,undo 是回滚”——这句话我听过不下二十遍,每次在面试里被问到,我都答得流利又自信。直到某次凌晨三点,线上一个关键事务卡在enq: TX - row lock contention上,监控显示log file sync平均耗时飙到 87ms,而undo_retention却被设成了 900 秒但实际v$undostat里unxpstealcnt持续非零。我一边查v$log_history,一边翻v$transaction,一边盯着v$session_longops里那个永远走不完的ROLLBACK进度条……突然意识到:我根本没真正理解 redo 和 undo 在磁盘、内存、事务、崩溃恢复这四层空间里到底怎么协同工作的。

这不是概念背诵题,这是数据库的呼吸节律。redo 记录的是“物理变化”,是 Oracle 把你改了哪一页、哪个块、哪个 offset 的字节写进了哪个日志文件;undo 记录的是“逻辑逆操作”,是“把 id=123 的 name 从 '张三' 改回 '李四'”这种可读指令。它们不是并列的两个模块,而是同一套事务机制的左右手:redo 负责“万一断电,数据能找回来”,undo 负责“万一你反悔,数据能退回去”。更关键的是——它们共享同一个底层资源池:SGA 中的 log buffer 和 undo buffer,以及磁盘上的 online redo log 文件和 undo tablespace 数据文件。你调大log_buffer,可能缓解log file parallel write等待,但若undo_tablespace所在磁盘 I/O 已饱和,rollback依然慢如蜗牛;你延长undo_retention,看似保护了闪回查询,但如果undo tablespace是autoextend且maxsize设得过大,又可能引发ORA-01555快照过旧错误——而这个错误的根因,往往藏在v$undostat的ssolderrcnt字段里,而不是在 alert.log 的第一行。

所以这篇不是教科书复述。我会带你从一次真实事务的完整生命周期切入:从INSERT INTO t1 VALUES (1, 'a')执行开始,到COMMIT成功返回,再到意外ROLLBACK或实例崩溃后重启恢复,全程拆解 redo 和 undo 在每个环节写了什么、存在哪、何时刷盘、如何索引、怎样冲突。所有内容基于 Oracle 19c(兼容 12c/19c/21c),所有命令、视图、参数、等待事件均来自生产环境实测,不引用任何文档截图,只告诉你“为什么必须这么查”“为什么不能那么设”。


2. redo 日志:不是流水账,而是带时间戳的物理变更快照链

2.1 redo 的本质:它记录的从来不是 SQL,而是数据块的二进制差分

很多人误以为 redo log 里存着UPDATE t1 SET name='b' WHERE id=1这样的语句。错。Oracle 从不记录 SQL 文本。它记录的是:某个数据块(data block)在某个时刻,从什么状态变成了什么状态。

举个最简例子:假设表t1的第 1 行存储在文件 4、块号 123(即file# = 4, block# = 123)中,该块当前在 buffer cache 里是干净的(clean)。执行UPDATE t1 SET name='b' WHERE id=1后,Oracle 做了三件事:

  1. 在 buffer cache 中修改该块:将原值'a'改为'b',此时该块变为“脏块”(dirty block);
  2. 生成一条 redo record:这条记录包含:
    • SCN(系统变更号):比如123456789,标记这次变更的时间戳;
    • OP code:11.2(表示“更新表数据块中的列值”);
    • Block address:file 4, block 123;
    • Change vector:精确到字节的差分信息,例如“offset 1024 处的 2 字节从 0x6161('aa')改为 0x6262('bb')”;
  3. 将该 redo record 写入 log buffer:注意,不是写入磁盘,只是写入 SGA 中的内存缓冲区。

提示:你可以用ALTER SYSTEM DUMP LOGFILE 'path/to/redo01.log' SCN MIN 123456789 SCN MAX 123456790;命令导出指定 SCN 区间的 redo 内容,再用strings或od -x查看原始二进制。你会看到大量0x0B02(OP code 11.2)、0x0004(file#)、0x007B(block# 123 十六进制)等字节序列——这才是 redo 的真实面目。

2.2 redo 的三层存储结构:log buffer → online redo log → archived log

Oracle 的 redo 不是单一层级,而是三级缓存结构,每一级解决不同问题:

层级存储位置刷盘触发条件核心作用DBA 关注点
log bufferSGA 内存LOG_BUFFER参数大小(默认约 5MB)、COMMIT、每 3 秒超时、buffer 满 1/3缓冲高频小事务,减少磁盘 I/O若log buffer太小,log buffer space等待上升;太大则增加log file sync延迟
online redo log磁盘文件(如redo01.log)LGWR 进程按需写入(commit 时强制刷)实例崩溃恢复的唯一依据;保证事务持久性(Durable)文件大小建议 ≥ 500MB;组数 ≥ 3;避免log file switch (archiving needed)等待
archived log磁盘归档目录(如/arch/)归档进程 ARCH 将 inactive 的 online log 复制一份支持时间点恢复(PITR)、Data Guard 同步、逻辑备库构建ARCHIVE_LAG_TARGET控制最大归档延迟;DB_RECOVERY_FILE_DEST_SIZE防止归档空间满

这里有个极易被忽略的关键点:LGWR 写入 online redo log 是“顺序写”,但写入速度受磁盘 I/O 能力硬限。如果你把所有 online redo log 文件都放在同一块 SATA 盘上,而业务峰值每秒产生 120MB redo,那log file sync等待必然飙升——因为 SATA 盘顺序写极限约 100~150MB/s,已无冗余。解决方案不是调大log_buffer,而是将 redo 文件分散到多块 NVMe SSD 上,并确保每个日志组至少有 2 个成员(mirroring)。

2.3 redo 的核心视图与诊断命令:从等待事件定位瓶颈

当应用抱怨“提交太慢”,90% 的情况要先看log file sync等待:

-- 查看当前会话等待 SELECT sid, event, p1text, p1, p2text, p2, seconds_in_wait FROM v$session WHERE event = 'log file sync'; -- 查看历史等待(AWR) SELECT event, time_waited_micro/1000000 as sec_waited, wait_count, avg_wait_ms FROM dba_hist_system_event WHERE event = 'log file sync' AND snap_id BETWEEN 1000 AND 1010;

log file sync的p1是buffer#(log buffer 的编号),p2是sync#(同步序号),但它本身不告诉你慢在哪。真正要查的是 LGWR 的行为:

-- 查看 LGWR 进程的 I/O 统计 SELECT name, value FROM v$sysstat WHERE name IN ('redo writes', 'redo write time', 'redo blocks written'); -- 计算平均每次写入耗时(单位:毫秒) -- redo write time (centiseconds) / redo writes = 平均每次写入耗时(厘秒)→ ÷10 得毫秒 SELECT ROUND((a.value / b.value) / 10, 2) AS avg_write_ms FROM v$sysstat a, v$sysstat b WHERE a.name = 'redo write time' AND b.name = 'redo writes';

如果avg_write_ms > 5,说明磁盘写入已成瓶颈。此时再查v$iostat_file:

SELECT filetype_name, small_read_reqs, small_write_reqs, ROUND(small_write_time/NULLIF(small_write_reqs,0),2) AS avg_write_ms FROM v$iostat_file WHERE filetype_name LIKE '%Redo Log%';

你会发现avg_write_ms与上面计算值高度一致——这就锁定了问题:不是 Oracle 配置问题,是存储硬件问题。此时调优方向只有一个:换更快的磁盘,或调整日志文件分布策略。

2.4 redo 的三个致命误区:DBA 踩坑最多的实操陷阱

误区一:“增大 log_buffer 就能降低 log file sync 等待”
错。log_buffer只影响“写入 log buffer”的内存拷贝速度,而log file sync等待发生在 LGWR 将 log buffer 刷入磁盘之后。增大log_buffer可能反而让单次刷盘量变大,加剧磁盘压力。实测数据:某系统log_buffer从 8MB 调至 32MB,log file sync平均等待从 3.2ms 升至 4.7ms。正确做法是:保持log_buffer在 8~16MB(足够容纳 1 秒内 redo),重点优化磁盘 I/O。

误区二:“online redo log 文件越大越好”
错。大日志文件虽减少log file switch频率,但会显著拉长实例恢复(Instance Recovery)时间。Oracle 崩溃重启后,SMON 进程需重放所有未 checkpoint 的 redo,日志越大,重放越久。官方建议:单个 online redo log 文件大小应控制在 1~2GB,确保MTTR(Mean Time To Recover)≤ 5 分钟。可通过V$INSTANCE_RECOVERY视图验证:

SELECT target_mttr, estimated_mttr, optimal_logfile_size FROM v$instance_recovery;

若estimated_mttr > target_mttr,说明日志文件过大或检查点频率过低。

误区三:“归档模式下,redo 就永远不会丢”
错。归档只保证 online redo log 被复制,但若归档进程失败(如归档目录满、网络中断),online redo log 会被覆盖,导致无法进行 PITR。必须监控v$archive_dest_status:

SELECT dest_name, status, error, failure_count, archived_thread#, archived_seq# FROM v$archive_dest_status WHERE status != 'VALID';

一旦failure_count > 0,立即告警。我们在线上部署了每 5 分钟巡检脚本,发现error字段含ORA-00257(归档空间满)时,自动清理 7 天前归档并触发短信通知。


3. undo 表空间:不是回收站,而是事务的时空穿梭机

3.1 undo 的双重身份:事务回滚 + 读一致性 + 闪回查询的统一载体

很多人把 undo 当作“回滚专用区”,这是最大误解。Oracle 的 undo 表空间承担三大不可替代职能:

  • 事务回滚(Rollback):ROLLBACK时,Oracle 读取 undo record,执行逆操作(如把'b'改回'a');
  • 读一致性(Read Consistency):SELECT查询时,若数据块已被其他事务修改但未提交,Oracle 自动从 undo 中构造“前镜像”(before image),让你看到事务开始那一刻的数据状态;
  • 闪回查询(Flashback Query):SELECT * FROM t1 AS OF TIMESTAMP SYSTIMESTAMP - INTERVAL '5' MINUTE,本质就是读取对应 SCN 的 undo 数据。

这三者共享同一套 undo 数据结构,但访问路径完全不同:回滚走UNDOsegment 的 active list,读一致性走UNDOsegment 的 uncommitted list,闪回查询走UNDOsegment 的 expired list。因此,undo_retention参数不是“保留多久”,而是“尽量保留多久”——当 undo 表空间空间不足时,Oracle 会优先覆盖expired状态的 undo,即使undo_retention设为 3600 秒。

3.2 undo 的物理结构:segment → extent → block 的三级嵌套

undo 表空间由多个UNDOsegment 组成,每个 segment 又由多个 extent 构成,每个 extent 包含固定数量的 data block。关键点在于:每个 undo segment 都有独立的事务槽(transaction slot)和回滚段头块(segment header block)。

当你执行BEGIN TRANSACTION,Oracle 在某个UNDOsegment 的 header block 中分配一个 slot,记录该事务的起始 SCN、状态(ACTIVE/EXPIRED/UNEXPIRED)、所用 undo block 范围。后续所有 DML 产生的 undo record,都按顺序追加到该 segment 的 extent 中。

这带来一个关键推论:undo 表空间的性能瓶颈不在总大小,而在 active undo segment 的并发争用。如果所有事务都挤在同一个 undo segment 上,enq: US - contention等待就会出现。Oracle 默认创建 10 个 undo segment,但高并发 OLTP 系统建议手动创建更多:

-- 查看当前 undo segment 数量与状态 SELECT segment_name, tablespace_name, status, ROUND(bytes/1024/1024, 2) AS mb_used FROM dba_rollback_segs r, dba_segments s WHERE r.segment_name = s.segment_name AND s.tablespace_name = (SELECT value FROM v$parameter WHERE name = 'undo_tablespace'); -- 创建额外 undo segment(需在 MOUNT 状态下) ALTER DATABASE ADD UNDO TABLESPACE undotbs2 DATAFILE '/u01/oradata/undotbs2.dbf' SIZE 2G AUTOEXTEND ON NEXT 100M MAXSIZE 10G;

3.3 undo 的核心参数与视图:从 ORA-01555 到闪回失效的全链路诊断

ORA-01555: snapshot too old是 DBA 最怕的报错之一。它的本质是:查询需要的前镜像(undo)已被覆盖。但覆盖原因有三类,必须区分:

类型触发条件关键指标解决方案
空间压力覆盖undo 表空间满,Oracle 被迫覆盖未过期 undov$undostat.unxpstealcnt > 0增大 undo 表空间;设置RETENTION GUARANTEE
过期覆盖undo record 已过undo_retention时间,且空间充足v$undostat.expblkrelcnt > 0延长undo_retention;禁用AUTOEXTEND防止无限增长
主动清理SMON 进程定期清理已提交事务的 undov$undostat.nospaceerrcnt = 0正常行为,无需干预

诊断必须从v$undostat入手,这是 undo 的“黑匣子”:

-- 查看最近 1 小时 undo 使用统计(每 10 分钟一个快照) SELECT begin_time, end_time, ROUND(maxquerylen/60, 2) AS max_query_mins, ROUND(tuned_undoretention/60, 2) AS tuned_retention_mins, unxpstealcnt, expblkrelcnt, nospaceerrcnt, ROUND(sum(undoblks)*8192/1024/1024, 2) AS undo_mb_used FROM v$undostat WHERE begin_time > SYSDATE - 1/24 ORDER BY begin_time;

重点关注三列:

  • unxpstealcnt:非零表示空间压力,需扩容;
  • expblkrelcnt:非零表示过期覆盖,需调undo_retention;
  • maxquerylen:当前最长查询耗时(秒),undo_retention应 ≥ 1.2 × 此值。

注意:tuned_undoretention是 Oracle 自动计算的建议值,但它只参考maxquerylen,不考虑闪回需求。若业务要求支持 24 小时闪回,undo_retention必须设为 86400,且开启RETENTION GUARANTEE(否则仍可能被覆盖)。

3.4 undo 的四大实战陷阱:那些让 DBA 加班到凌晨的配置雷区

陷阱一:“undo_tablespace 设为 autoextend,maxsize 设为 UNLIMITED”
表面看很安全,实则埋下定时炸弹。当业务突发大量 long-running query,undo 持续增长,undotbs1.dbf文件不断膨胀,最终占满整个文件系统。更糟的是,autoextend导致文件碎片化,I/O 性能下降。我们曾遇到一个 2TB 的 undo 文件,v$undostat显示unxpstealcnt=0,但expblkrelcnt持续上升——因为autoextend让 Oracle 认为“空间永远够”,于是大胆覆盖过期 undo,导致闪回查询失败。正确做法:固定大小 + 手动监控 + 定期收缩:

-- 检查 undo 文件是否 autoextend SELECT file_name, bytes/1024/1024 AS mb, autoextensible, maxbytes/1024/1024 AS max_mb FROM dba_data_files WHERE tablespace_name = (SELECT value FROM v$parameter WHERE name = 'undo_tablespace'); -- 关闭 autoextend(需先 resize 到合理大小) ALTER DATABASE DATAFILE '/u01/oradata/undotbs1.dbf' AUTOEXTEND OFF;

陷阱二:“用 ALTER TABLE ... MOVE 压缩表,却忘了它会产生巨量 undo”
MOVE操作本质是CREATE TABLE AS SELECT+DROP+RENAME,它会为整张表的每一行生成 undo record。一张 1 亿行的表,MOVE可能产生 50GB undo。若undo_retention设为 3600 秒,这 50GB undo 会锁定 1 小时,期间所有长查询都可能报 ORA-01555。正确做法:分批处理 + 降低事务粒度:

-- 方案1:用 DBMS_REDEFINITION(在线重定义,undo 量小) EXEC DBMS_REDEFINITION.START_REDEF_TABLE('SCOTT', 'T1', 'T1_INTERIM'); -- 方案2:分批 MOVE(每次 100 万行) ALTER SESSION ENABLE PARALLEL DML; INSERT /*+ APPEND */ INTO t1_new SELECT * FROM t1 WHERE rowid BETWEEN 'AAABBB...' AND 'AAABBB...'; COMMIT;

陷阱三:“在 RAC 环境中,所有节点共用一个 undo tablespace”
错。RAC 中每个实例必须有独立的 undo tablespace。若共用,会出现enq: US - contention和gc cr block busy等跨节点争用。必须为每个实例创建专属 undo:

-- 实例1(inst_id=1) CREATE UNDO TABLESPACE undotbs1 DATAFILE '/u01/oradata/undotbs1_1.dbf' SIZE 4G; -- 实例2(inst_id=2) CREATE UNDO TABLESPACE undotbs2 DATAFILE '/u01/oradata/undotbs2_1.dbf' SIZE 4G; -- 分别设置 ALTER SYSTEM SET undo_tablespace='UNDOTBS1' SCOPE=BOTH SID='orcl1'; ALTER SYSTEM SET undo_tablespace='UNDOTBS2' SCOPE=BOTH SID='orcl2';

陷阱四:“认为 flashback database 不依赖 undo,所以不监控 undo 空间”
大错。FLASHBACK DATABASE依赖flashback logs,但flashback logs的生成前提是:所有 DML 的 undo record 必须先写入 undo tablespace,然后由 RVWR(Recovery Writer)进程异步复制到db_recovery_file_dest。如果 undo 表空间 I/O 拥塞,RVWR 无法及时读取 undo,flashback logs就会断流,V$FLASHBACK_DATABASE_LOG中oldest_flashback_scn停滞不前。必须同时监控:

-- 检查 flashback logs 是否正常生成 SELECT oldest_flashback_scn, oldest_flashback_time, retention_target, flashback_size, estimated_flashback_size FROM v$flashback_database_log; -- 检查 RVWR 进程状态 SELECT program, status, last_call_et FROM v$process WHERE program LIKE '%RVWR%';

4. redo 与 undo 的协同作战:一次 COMMIT 背后的七步精密交响

4.1 COMMIT 不是原子操作,而是 redo 和 undo 的七阶段握手协议

很多人以为COMMIT是瞬间完成的。实际上,从你敲下回车到客户端收到“Commit complete”,Oracle 至少执行了 7 个严格时序的步骤,redo 和 undo 在其中交替主导:

步骤主导方关键动作持有锁/资源耗时占比失败后果
1. 生成 commit redo recordLGWR在 log buffer 中写入OP:11.2(commit 标记)+ SCN无<1%事务仍可 rollback
2. LGWR 刷盘LGWR将 log buffer 中所有该事务的 redo(含 commit record)写入 online redo log持有log file synclatch60~80%log file sync等待
3. 更新 transaction tableSMON在 undo segment header 中将事务状态从 ACTIVE 改为 COMMITTED持有USenqueue<5%事务状态不一致
4. 释放行锁PMON清除v$lock中该事务的 TM/TX 锁持有TXenqueue<5%其他会话阻塞
5. 标记 undo 为 expiredSMON将该事务占用的 undo block 标记为UNEXPIRED(可被覆盖)持有USenqueue<5%闪回查询失效
6. 清理 buffer cacheDBWn异步将脏块写入 datafile(非必须)无异步无直接影响
7. 返回客户端Server Process发送成功响应无<1%无

关键洞察:步骤 2(LGWR 刷盘)是 COMMIT 的真正分水岭。只要这一步完成,事务就永久生效(Durability),即使实例立刻崩溃,重启后也能通过 redo 重放恢复。步骤 3~5 是“善后”,不影响持久性,但影响并发性和闪回能力。

4.2 从 v$transaction 看透事务生命周期:active → committed → expired 的状态流转

v$transaction是观察 redo/undo 协同的显微镜。执行以下实验:

-- 会话1:开启事务但不提交 INSERT INTO t1 VALUES (2, 'b'); -- 此时 v$transaction 显示: -- XIDUSN=8, XIDSLOT=12, XIDSQN=12345, STATUS='ACTIVE', USED_UBLK=1, USED_UREC=1 -- 会话2:查询该事务的 undo 使用 SELECT usn, slot, sqn, name, status, undoblocks FROM v$rollname r, v$rollstat s WHERE r.usn = s.usn AND r.usn = 8; -- 会话1:执行 COMMIT COMMIT; -- 此时 v$transaction 中该行消失,但 v$rollstat 显示: -- usn=8, status=1(ONLINE),undoblocks 未清零(仍为 1) -- 等待 10 秒后查 v$rollstat: -- undoblocks 变为 0,status 仍为 1

这个过程揭示了核心机制:COMMIT后,undo block 并未立即释放,而是进入UNEXPIRED状态,等待 SMON 定期清理。v$rollstat.undoblocks从 1 降到 0 的时间,就是undo_retention的实际生效窗口。这也是为什么undo_retention设为 900 秒,但v$undostat.maxquerylen只有 600 秒时,expblkrelcnt仍为 0——因为 SMON 还没来得及清理。

4.3 崩溃恢复(Instance Recovery):redo 重放 + undo 回滚的双引擎启动

实例崩溃后,Oracle 启动时的恢复流程是 redo 和 undo 协同的巅峰体现:

  1. Cache Recovery(前滚):SMON 读取 online redo log,将所有已写入 redo 但未写入 datafile 的脏块(dirty block)重新应用(apply),使 buffer cache 达到崩溃前一刻的状态;
  2. Transaction Recovery(回滚):SMON 扫描所有UNDOsegment,找到所有STATUS='ACTIVE'的事务(即崩溃时未提交的事务),读取其 undo record,执行逆操作,将这些事务的修改全部撤销。

提示:V$RECOVERY_PROGRESS显示恢复进度。TYPE='CACHE RECOVERY'阶段快,TYPE='TRANSACTION RECOVERY'阶段慢——因为回滚需要随机读取 undo block。若TRANSACTION RECOVERY耗时过长,说明有大量未提交事务,应检查应用是否有BEGIN后漏COMMIT的代码。

4.4 生产环境黄金配置清单:经 12 个核心系统验证的参数组合

所有参数必须根据你的硬件和业务负载调整,以下是我们在线上稳定运行 3 年的基准配置(Oracle 19c,OLTP 为主):

参数推荐值为什么这样设验证方法
log_buffer12M平衡内存占用与刷盘频率;实测 8M 下log buffer space等待上升v$sysstat中redo log space requests= 0
fast_start_mttr_target300(5分钟)控制检查点频率,避免崩溃恢复过长v$instance_recovery.estimated_mttr ≤ 300
undo_managementAUTO强制使用 undo tablespace,避免手动管理风险v$parameter.undo_management = 'AUTO'
undo_tablespaceUNDOTBS1(专用)避免与其他表空间争用 I/Ov$datafile中文件路径独立于 datafile
undo_retention3600(1小时)覆盖 99% 的长查询;RAC 环境每实例独立v$undostat.maxquerylen的 P95 值 × 1.2
undo_tablespace大小≥ 20G(固定)防止 autoextend 导致失控;20G 可支撑 1 小时 1000TPSdba_free_space中剩余空间 > 30%
db_writer_processes4(CPU 核数 ≥ 16)加速 dirty block 写入,减轻 DBWn 压力v$sysstat中db block gets from cache稳定

注意:fast_start_mttr_target不是目标值,而是 Oracle 的“调控杠杆”。设为 300 后,Oracle 会自动调整检查点频率,使estimated_mttr尽量接近 300。若estimated_mttr持续 > 400,说明日志文件过大或 I/O 不足,需优先优化存储。


5. 故障排查实战:从一条慢 SQL 定位 redo/undo 根因的完整链路

5.1 场景还原:一个 UPDATE 语句执行 47 秒,却没有任何锁等待

某日,监控报警:UPDATE t_order SET status='SHIPPED' WHERE order_id IN (SELECT order_id FROM t_shipment WHERE ship_date < SYSDATE-7)执行超时。v$session显示event='log file sync',但v$session_wait的p1(buffer#)为 0,p2(sync#)为 0——这很反常,因为正常log file sync的p1/p2应是非零值。

第一步:确认是否真在等 redo

-- 查看该会话的等待链 SELECT event, p1text, p1, p2text, p2, p3text, p3, seconds_in_wait FROM v$session_wait WHERE sid = 1234; -- 结果:event='log file sync', p1text='buffer#', p1=0, p2text='sync#', p2=0

p1=0意味着 LGWR 没有为该会话分配 log buffer slot?不可能。继续查 LGWR 进程:

-- 查看 LGWR 的 I/O 状态 SELECT event, p1text, p1, p2text, p2, seconds_in_wait FROM v$process p, v$session s WHERE p.addr = s.paddr AND s.program LIKE '%LGWR%'; -- 结果:event='rdbms ipc message', seconds_in_wait=0 —— LGWR 空闲!

LGWR 空闲,但会话却在等log file sync?矛盾。再查v$transaction:

SELECT xidusn, xidslot, xidsqn, start_scn, used_ublk, used_urec, log_io, phy_io FROM v$transaction WHERE ses_addr = (SELECT saddr FROM v$session WHERE sid = 1234);

发现used_ublk=12000,used_urec=150000——这个事务产生了 12000 个 undo block!而t_order表只有 500 万行,t_shipment有 200 万行,子查询结果集应 ≤ 10 万行。问题出在子查询未走索引,导致全表扫描t_shipment,并为每一行生成 undo(因为UPDATE的 where 条件是子查询,Oracle 必须先执行子查询,再对结果逐行 update)。

第二步:验证执行计划

EXPLAIN PLAN FOR UPDATE t_order SET status='SHIPPED' WHERE order_id IN (SELECT order_id FROM t_shipment WHERE ship_date < SYSDATE-7); SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- 结果:t_shipment 走全表扫描,t_order 走全表扫描(no index on order_id)

第三步:根因定位

  • t_shipment.ship_date无索引 → 全表扫描 → 产生海量 undo
  • t_order.order_id无索引 →UPDATE无法定位行 → 对全表每行判断order_id IN (...)→ 每行都生成 undo record

第四步:修复

-- 添加缺失索引 CREATE INDEX idx_shipment_date ON t_shipment(ship_date); CREATE INDEX idx_order_id ON t_order(order_id); -- 重写 SQL(避免子查询) MERGE INTO t_order o USING (SELECT order_id FROM t_shipment WHERE ship_date < SYSDATE-7) s ON (o.order_id = s.order_id) WHEN MATCHED THEN UPDATE SET o.status = 'SHIPPED';

修复后,执行时间从 47 秒降至 0.8 秒,used_ublk从 12000 降至 12。

经验:当log file sync等待异常且p1=0时,不要只盯 LGWR,要查v$transaction.used_ublk。如果它远大于预期,说明 DML 本身产生了过多 undo,问题在 SQL 写法或索引缺失,而非 redo 配置。

5.2 场景还原:RAC 环境下,节点1的查询报 ORA-01555,节点2一切正常

现象:节点1执行SELECT COUNT(*) FROM t_large(耗时 8 分钟)时,报ORA-01555: snapshot too old;节点2 执行

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询