ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Oracle一致性读(Consistent Read)深度解析:ORA-1555不是Bug,是Oracle的“安全阀“

Oracle一致性读(Consistent Read)深度解析:ORA-1555不是Bug,是Oracle的“安全阀“ 前言先说一个被误解了很多年的报错ORA-1555 snapshot too old。几乎每个 DBA 都被它咬过也几乎每个人第一反应都是这破玩意是不是 bug。我的看法正好相反ORA-1555 不是 bug它是 Oracle 的一致性读机制给你守的底线——宁可报错也不给你一份拼错了的过去。它是安全阀不是故障。为什么这么说看完这篇你就明白了。一致性读这套机制做的就是一件不可能的事让一条 SELECT 看到一个根本不存在的、冻结在过去的瞬间的世界。undo 被覆盖、事务表被复用、undo 段被 drop——每一步都可能拼不回去。拼不回去的时候Oracle 选择喊停1555而不是糊弄你一个错的结果。这就是安全阀三个字的全部含义。上一篇聊块清除收尾的时候我留了个扣子fast cleanout 在 ITL 里写的 commit SCN 是个upperbound——事务在这个点之前提交了但它不说具体哪个点。那问题来了当一条 SELECT 要重建某个历史版本时到底凭什么判断这行数据我该看哪个版本这个问题就是一致性读要回答的也是这个系列的收官篇。先从一个你每天都在经历、但未必细想过的场景说起。你 10 点整发出一条 SELECT这条语句要扫一百万行得跑三分钟。10 点 01 分有人改了其中一行并提交了。10 点 03 分你的查询扫到那一行——你看到的还是旧值。不是新值也不是报错就是 10 点整那个瞬间的旧值仿佛时间在你发出 SELECT 那一刻被冻住了。Oracle 是怎么做到的它没给表加锁没挡住那个改数据的人也没把整表复制一份。答案藏在这八个字里块级多版本 undo 重建。前两篇我们拆了 DDL 的特殊事务、拆了 commit 之后的块清除今天这块拼图放上去事务层的图景就完整了。一、先把几个名词摁住一致性读的文档和 trace 里有一堆缩写先把人认全后面才不晕。ITL、Xid、Uba 这些前两篇讲过一句话带过ITL 是块头里的事务登记表Xid 指向 undo 段头事务表的槽位Uba 指向 undo 记录。新面孔是下面这几位名词含义current 块buffer cache 里块的最新版本改数据用的就是它CR 块块的一致性版本Consistent Read copy只包含某个 SCN 之前已提交的修改Snap_SCN快照 SCN查询开始那一刻的参考点整条语句就认这个时刻Env_SCN环境 SCN数据库现在走到哪了CR_SCNbuffer header 里记的这个版本的一致性基准点。current 块恒为 -1表示最新CR_XID / CR_UBA最近改/重建过这个版本的事务 ID 和最后一条 undo 的地址CR_SFL一个标志位置位表示至少两个不同事务在这个版本上被回滚过块从此私有化核心思想一句话你的 SELECT 拿着 Snap_SCN 这把尺子去量每一个块凡是 Snap_SCN 之后的修改统统用 undo 记录倒放回去。看个最小例子。T1 在 SCN 30 提交T2 在 SCN 31 提交对同一个块做了两次 update。T3 的 SELECT 在 SCN 30.5 启动Snap_SCN 大致就是 30。那么 T3 扫到这个块时T1 的修改要看见T2 的两次 update 必须看不见——怎么办把 T2 的 undo 记录一条条应用回去把这个块演回 SCN 30 的样子。这个演出来的副本就是 CR 块。注意两件事第一current 块本身绝不动回滚是在克隆出来的副本上做的第二为一致性读应用的 undo 不产生 redo——这不是真的改数据只是内存里搭了个历史布景。二、主干流程拿到一个块要过的六道关你发起 SELECTOracle 要为每个数据块做一次CR Get内核里大致是 kcbgetcr() 这一族函数。我把整个决策过程捋成六步第一步先在 buffer cache 里淘现货。顺着 hash 链找这个块的已有 CR 版本要求是 CR_SCN Snap_SCN——版本必须够新不然 Snap_SCN 之前的某些提交可能不在里面会丢修改。候选有好几个的话挑 CR_SCN 最接近 Snap_SCN 的那个因为差距越小要倒放的 undo 越少。第二步没有现货从磁盘读 current 块。current 块的 CR_SCN 视为无穷大——它包含一切修改永远合适但它是全库的独一份不能直接给你用得先克隆。第三步现货合格就直接用。找到的版本已一致、是 CR 模式、没被别的事务锁住——拿来就读这是最快路径统计里的 consistent gets 大多走的是这条路。第四步不合格就加工。典型情况是选中的是 current 块你持有的是 EXCL 模式而 CR 访问要 SHARE 模式所以克隆一份再加工。第五步扫 ITL 验证一致性。拿到最佳候选后扫一遍块头 ITL有没有没写 commit SCN 的事务有就先做 block cleanout上一篇的老朋友下文细说。第六步逐条倒放 undo。不需要 cleanout但版本的 CR_SCN 比 Snap_SCN 新——说明里面混着 Snap_SCN 之后的修改。于是一次选一个 ITL 条目、一次应用一条 undo 记录循环直到整个块相对 Snap_SCN 干净为止。如果中途发现需要的 undo 已经被覆盖找不回来了——恭喜你喜提ORA-1555 snapshot too old。六步走完你手里拿到的就是一个时间冻结在 Snap_SCN 的块。旁枝一hash 链不能无限长一个块每被不同 Snap_SCN 的查询重建一次链上就多一个版本。kcbgetcr() 会顺手修剪同一个块的 CR 版本默认最多留 6 个只读模式下更省2 个由隐藏参数 _db_block_max_cr_dba 控制。这个参数我一般不建议碰但你得知道它的存在。当年我遇到过一个诡异的 buffer cache 抖动一条跑了很久的报表查询配上热点表的高频小事务CR 版本不停地建了删、删了建consistent gets 和 latch 竞争一起飙。排查到最后才明白长查询把 Snap_SCN 摁在很久以前而热点块早被后续事务改得面目全非——每次访问都要从 current 克隆重建建好了还很快被修剪掉纯粹的内耗。这类现场的解药从来不是调参数而是让长查询和热点写入错开。旁枝二RAC 下更绕单实例找不到现货就从磁盘读RAC 里还得先想想我要的版本会不会在别的节点的 cache 里跨节点请求、版本协商逻辑链条更长。这也是为什么 RAC 上长查询对 CR 的扰动会被放大——但那是另一个大话题今天不展开。三、凭什么这个版本能用最佳候选的验证第一步说挑 CR_SCN 最接近的其实没那么简单。CR Get 选出现货之后还要过一道验证尤其当你自己这个事务也改过数据的时候。先明确一条基本规则会话能看到别人在 Snap_SCN 之前提交的一切自己在 Snap_SCN 之前做的未提交修改自己也得看见——比如你 update 了一张表还没提交紧接着 select 它你当然要看自己改过的样子。语句级一致性管别人的事事务级连续性管自己的事。验证逻辑分几种情况当前事务的 UBA 记作 Env_UBA为 0 表示本事务没改过东西Env_UBA 0自己没做过修改只验CR_SCN Snap_SCN就够了最省事CR_XID 0这个版本自构建以来没人再碰过、也没套用过 undo里面的修改都是原装的没漏CR_XID ≠ 我的 XID且 CR_SFL 0这个版本只被回滚过一个事务的修改其他事务包括我的修改都还在也没漏CR_XID 我的 XID且 Env_UBA CR_UBA我在这个版本上被部分回滚过我的一些修改不在里面了——但只要那些被倒放的修改发生在我的 Snap_UBA 之后本来就轮不到我看见依然合格。这套判断看着绕核心就一句话这个版本里该在的修改Snap 之前的都在不该在的Snap 之后的都不在。验不过就换下一个候选或者重建。四、克隆的规矩buffer 的六种访问模式前面反复说克隆这里把 buffer 的访问模式交代清楚。一个 buffer 在同一时刻处于且仅处于一种模式模式用途NULL内部 cache 层自己用SHR共享读 current 块EXCL排他写 current 块改数据必须这个CR共享读一致性版本CRX一种类 CR 模式专门用于回滚段头NEW创建新块的排他模式比如块分裂、段扩展关键点在于改块永远要 EXCL 拿 current读历史永远走 CR 拿副本。所以哪怕你的 SELECT 想要的内容就在 current 块里一字不差流程上也不能直接读它——得克隆出副本、置成 CR 模式、在副本上做验证和回滚。current 块是生产的唯一现场谁都不能围着一个正在施工的现场读历史。副本建好之后buffer header 里会记下这个版本的身份证。dump 出来长这样X$BH 里也能看到对应字段st: CR, cr:[[scn: 0x0000.00078bc4],[xid: 0x0002.008.000000a9],[uba: ...], sfl: 0x0]stCR 表明身份scn 就是这个版本的 CR_SCNxid/uba 记录最近是谁重建或改动过它sfl0 表示还能被大家共享——关于它什么时候会变成 1后面讲。五、cleanout 又来敲门两个非清不可的场景上一篇讲过commit 之后 ITL 里可能还留着没清理的事务信息。一致性读扫 ITL 时有两种情况会触发 cleanout而且是互斥的两种场景一块的有效清除 SCN块头里的 KTBBHCSC比 Snap_SCN 还旧。说明这个块的 ITL 信息停留在很早以前得先 cleanout 把它刷新到位。场景二ITL 里有个事务已提交但 commit SCN 是个估计值upperbound而且这个估计值比 Snap_SCN 大。这就回到了上篇结尾的那个扣子——upperbound 只说在这之前提交的可 Snap_SCN 也在这个之前的范围里到底谁前谁后说不清。说不清就不能用必须 cleanout 去 undo 段头把精确值取回来。取精确值本身也要做一致性读——undo 段头的事务表同样得按 Snap_SCN 重建版本用上面提到的 CRX 模式访问consistent gets 照样计数。查到的信息会暂存在 cleanout info area 里注意对非 current 块做这些动作是不产生 redo 的。还有一种特殊情形undo 段整个被 drop 掉了。Oracle 的做法是把 UNDO$ 里对应行更新而不是删除——STATUS$ 置为 1invalidSCNBAS/SCNWRP 记下 drop 之前最近的 commit SCN。于是一致性读遇到指向已消失 undo 段的 Xid 时段在 Snap_SCN 之前被 drop那个事务必然在 Snap_SCN 之前就提交了不然段不敢 dropITL 里填个近似 commit SCN 即可段在 Snap_SCN 之后被 drop过去的世界缺了一块拼图——ORA-1555。六、主戏ITL 扫描与逐条回滚主戏登场ITL 扫描与逐条回滚好现在假设 cleanout 做完了ITL 里每个事务的去向都明确了。接下来逐条扫描判断这个块相对 Snap_SCN 是否一致。下面三条满足任意一条就得回滚存在别的活跃事务还没提交修改当然不能给你看活跃事务是我自己但它的修改发生在我的 Snap_UBA 之后那是未来的我干的不该看存在 Snap_SCN 之后才提交的事务——不管 ITL 里写的是精确 SCN 还是 upperbound 估计值只要越过 Snap_SCN 这条线就得倒放。回滚的操作顺序一个块可能有多个 ITL 条目要回滚先动哪个Oracle 的优先级是第一个活跃事务按 ITL 条目顺序没有活跃的了选 Snap_SCN 之后提交、且 SCN 最大的那个——越晚提交的修改越表层倒放要从最外层剥起SCN 相同优先取有精确 commit SCN 的。选定一个 ITL 条目后顺着它的 Uba 链一条一条应用 undo 记录每应用一条统计项 data blocks CR - undo records applied 加一。什么时候换下一个条目三种情况这个 XID 的修改全部倒放完了ITL 条目失效倒放过程中 ITL 条目被重用、指向了另一个事务条目指向的是当前事务自己且已经回滚到 Snap_UBA 之下——够了自己的旧账翻到这为止。三条都不满足、也没有更多越线的事务——块一致了收工。一个完整的回滚现场看个具体例子。某块有三个 ITL 条目我的查询 Snap_SCN 31ITL1别人的事务先回滚它。顺着 Uba 链应用两条 undo 记录后这个条目失效——XID1 的修改清干净了。ITL3另一个事务SCN 33 提交比 31 晚也在回滚名单里。应用两条 undo 之后这个条目指向了链条上前一个占用者 XID3——一查XID3 在 SCN 27 提交早于 Snap_SCN它的修改本来就该被看见停手。ITL2指向我自己的事务而我的修改都在 Snap_UBA 之前——不用动。收工时盘点这个副本上回滚过两个不同事务的修改。于是 CR_SFL 置位——这个 CR 块从此变成发起事务私有的版本buffer header 里 sfl1别的事务不能再来共享它。道理也简单这个版本是按我的 Snap_SCN 和我的未提交修改量身定制演出来的布景里掺了只对我有意义的私货别人进来看到的世界是错的。所以前面 X$BH 里那个 CR_SFL 字段0 表示可共享、1 表示私有——它不只是一个标志它决定了这个副本的公用品还是私人物品属性也影响着 hash 链上版本的复用效率。最后补一个小知识通过可传输表空间刚搬过来的块不做这套回滚——传输的前提就是所有事务都已提交Oracle 直接信任。七、事务表里的时间区间[lowtime, hitime]上一篇讲 deferred cleanout 时提过查一个事务到底提交了没有得按 Xid 摸到 undo 段头以 CRX 模式访问事务表。但事务表也是个块也有一致性问题hash 链上可能挂着 current 加好几个快照版本每个版本只覆盖一段 SCN 区间记作 [lowtime, hitime]lowtime事务表里的 KTUXCSCN。槽位被复用是常态凡是槽位被复用过的已提交事务其 commit SCN 都不超过这个值——它是已知世界的下边界hitime这个 buffer 版本的 CR_SCNcurrent 块为 -1用数据库最近 SCN 代替——它是这个版本能回答问题的上边界。拿着 Snap_SCN 对号入座槽位被复用且 Snap_SCN lowtime问不出来了但可以安全地给一个近似答案——commit SCN 就标 lowtime反正真值更早早于 Snap_SCN够用了槽位没被复用状态是已提交直接拿到精确 commit SCN最理想事务看着还活跃但 Snap_SCN hitime这个版本太旧了它记录的活跃是过去式也许人家在 Snap_SCN 之前早提交了——换更新的版本看。两个棘手工况事务表太旧版本能回答的上限 hitime 比 Snap_SCN 还小而那个事务在这个版本里还显示活跃。没法判断 Snap_SCN 时刻的真实状态只能往更新的版本找。事务表太新所有版本的 [lowtime, hitime] 都不覆盖 Snap_SCN而且那个槽位在每个版本里都已被复用。这时留 lowtime 最接近 Snap_SCN 的版本然后干一件狠事——回滚事务表本身顺着事务表自己的 undo 链往过去倒直到找到答案。停止条件有三个倒放到某个历史版本槽位的 wrap# 跟 Xid 对上了——拿到精确 commit SCN圆满事务表的 lowtime 已经 ≤ Snap_SCN——可以安全判定早就提交了用近似值KTUXCUBA 0——undo 段被销毁重建过链断了ORA-1555。这套操作也有专门的统计transaction tables consistent reads - undo records applied按 undo 记录条数计和 transaction tables consistent read rollbacks按回滚次数计。老规矩这些 undo 应用都不产生 redo。还有一个务实的优化一致性读扫一张大表跨行跨块会反复遇到同一个 Xid。Oracle 会把查出来的 commit time 缓存起来下次再遇到同一个 Xid 直接用不重复摸事务表。没有这层缓存大表扫的成本会难看得多。八、ORA-1555那句让人血压升高的报错前面埋了三处 ORA-1555现在可以回答开头那个问题了为什么说 1555 是安全阀而不是 bug先把根因摁住undo 段是环形结构事务提交之后它的 undo 空间就可以被后来的活跃事务覆盖事务表槽位就那么几十个复用是常态。你的长查询想要的历史一旦被碾过去就再也拼不回去了。注意 Oracle 在这个时刻的选择它手里有 current 块最新数据有 Snap_SCN你要的过去但中间的 undo 链断了。摆在它面前两条路——要么给你一份残缺拼凑的结果用最新数据冒充历史数据你的报表数字对不上账你都不知道要么明明白白告诉你过去拼不回去了。Oracle 选了后者。1555 的本质是数据正确性的最后防线宁可让你重跑绝不给你错的数据。这不是失败是守住了底线。当然理解归理解中招的时候血压该高还是高。手动管理 undo 的年代这玩意儿是家常便饭。到了 AUM自动 undo 管理情况好得多靠的是两条腿段分配算法尽量一个事务分一个段减少互相踩踏UNDO_RETENTION 与自动 tuned retention保留期内的 undo 尽量不被覆盖retention 还会按系统负载自动调。但少得多不等于没有。undo 表空间太小、长查询碰上批量并发写入、retention 没兜底没设 guarantee照样会中招。救火的基本功还是那几样扩 undo、拆长查询、错开高峰。想深挖的话MOS 上 10630.1、45895.1、40689.1 这几篇老笔记依然值得一读。九、监控视图速查理论讲完上几样顺手的观察工具排障时基本就靠它们V$UNDOSTAT——undo 健康度的总仪表盘每 10 分钟一行SELECT begin_time, undoblks, txncount, maxquerylen,ssolderrcnt, -- ORA-1555 发生次数nospaceerrcnt, unxpstealcnt, expstealcntFROM v$undostatORDER BY begin_time DESC;maxquerylen最长查询秒数对上去对比 tuned retention就能判断 undo 撑不撑得住你的最长查询ssolderrcnt 非零就说明 1555 真的来过。DBA_UNDO_EXTENTS——看 extent 级别的状态ACTIVE有活跃事务在用、EXPIRED过了保留期可被 SMON 回收、UNEXPIRED还在保留期内、再加上 HHWM 之上的未定义部分。如果 UNEXPIRED 堆积把表空间撑满说明保留策略和容量不匹配。X$BH——buffer 级别的显微镜本篇主角都在这里​​​​​​​SELECT hladdr, dbarfil, dbablk, state, mode_held,cr_scn_bas, cr_scn_wrp,cr_xid_usn, cr_xid_slt, cr_xid_sqn,cr_uba_fil, cr_uba_blk, cr_sflFROM x$bhWHERE state 3; -- 3 CRcr_sfl 为 1 的就是被私有化的版本。想观察某个热点块挂了多少 CR 版本按 dbarfil/dbablk 分组 count 一下即可——别忘了 _db_block_max_cr_dba 那个上限。V$TRANSACTION——START_SCN、START_UBA、USED_UBLK/USED_URECundo 用量还有 CR_GET、CR_CHANGE 两个跟一致性读直接相关的计数。其余两个老朋友V$ROLLSTAT 看 undo 段的事务数与竞争XACTS/GETS/WAITSV$LOCKED_OBJECT 看锁。UNDO$ 的 STATUS$ 含义也留个底1 invalid / 2 defined / 3 online / 4 offline / 5 needs recovery / 6 partially available——前面讲undo 段被 drop 后行不删只置 1说的就是它。十、动手实验用闪回亲手看见过去一致性读最直观的体验其实是闪回Flashback家族——它们底下跑的就是同一套 CR 机制。下面四个实验在 SCOTT 环境里就能玩建议按顺序做。实验一Flashback Query把删掉的世界看回来。​​​​​​​CONNECT scott/tigerSELECT systimestamp FROM dual; -- 记下这个时间戳 T0UPDATE emp SET job SUPPORT WHERE job CLERK;COMMIT;-- 过几分钟再回头看 T0 时刻的世界SELECT * FROM empAS OF TIMESTAMP to_timestamp(T0, DD-MON-YY HH24.MI.SS.FF);CLERK 回来了。你刚才读的每一个块都是按 T0 那个 Snap_SCN 重建出来的 CR 版本。实验二Flashback Versions Query看一行的变迁史。​​​​​​​SELECT ename, job,versions_startscn, versions_starttime,versions_endscn, versions_endtime,versions_xid, versions_operationFROM emp VERSIONS BETWEEN TIMESTAMPsystimestamp - interval 10 minuteAND systimestamp - interval 1 minuteWHERE ename SMITH;每个版本一行起止 SCN、事务 ID、操作类型俱全。注意 versions_xid——它就是 ITL 里那个 Xid前两篇的老朋友。实验三Flashback Table整表倒带。​​​​​​​ALTER TABLE emp ENABLE ROW MOVEMENT; -- 前置条件FLASHBACK TABLE empTO TIMESTAMP systimestamp - interval 10 minute;实验四Flashback Transaction Query让 Oracle 替你写补偿 SQL。先开补充日志不开的话 undo_sql 列会是 UNKNOWN/NULLALTER DATABASE ADD SUPPLEMENTAL LOG DATA; -- sysdba另一个会话做点 update 并 commit然后捞 XID​​​​​​​SELECT versions_xidFROM scott.emp VERSIONS BETWEEN TIMESTAMPsystimestamp - interval 5 minute AND systimestampWHERE versions_xid IS NOT NULL;SELECT xid, start_scn, commit_scn, logon_user, undo_sqlFROM flashback_transaction_queryWHERE xid HEXTORAW();undo_sql 列就是逐行的反向操作——它正是从 undo 记录里反解出来的。进阶一步把 versions_xid 拆开亲手 dump 那块 undo。查出来的 versions_xid 是拼起来的十六进制串字节序要转换。比如 Linux 上 050018005D080000实际对应 0005.0018.0000085D——即 usn5、slot24、wrap21410x85D。拿着这组数直接 dumpALTER SYSTEM DUMP UNDO BLOCK _SYSSMU5_... XID 5 24 2141;trace 里就是这个事务在事务表槽位里的完整档案。动作要快——前面说了槽位复用是常态过一阵这个槽位换了主人就 dump 不出原来的东西了。想再往前挖还可以用前两篇练过的手法dump 数据块看 ITL、查 undo$ 定位段头、event 10203 追 cleanout配合着把整条链路走一遍。总结回到开头的两个问题现在可以一并回答了。你那条 SELECT 是怎么看见过去的它拿着 Snap_SCN 这把尺子在 buffer cache 里淘现货或克隆副本扫 ITL 认出哪些事务越过了时间线该 cleanout 的先 cleanout然后顺着 Uba 链一条一条倒放 undo遇到事务表的历史问题就用 [lowtime, hitime] 区间对号入座必要时连事务表本身一起回滚直到整个块干净地停在你要的那个瞬间。而 ORA-1555 是什么时候登场的就是这套精密还原术里任何一环的历史被覆盖、实在拼不回去的时候。它站出来喊停告诉你过去是有保质期的。给你报错是因为它给不了你正确答案——这正是安全阀的意义。这个系列到这里五篇拼成了一张完整的图第一篇讲 SCN、Undo、ITL 这三块基石——把事务层的地基摆上台面后面所有的故事都建立在这三个概念之上第二篇讲一条 UPDATE 的内核之旅——跟着最普通的 UPDATE 走一遍全流程看事务引擎是怎么点火、运转、熄火的第三篇讲 DDL 与特殊事务——事务层怎么为结构性动作开特例递归事务、独立事务这些旁门是怎么跟主事务共存的第四篇讲块清除——commit 之后块里的战场谁来打扫fast 与 deferred 怎么分工upperbound SCN 从哪来这一篇讲一致性读——查询怎么借助 undo 把过去的某一瞬间精确还原出来那个 upperbound 又是如何被收口成精确答案的以及拼不回去的时候 1555 如何守住底线。五篇下来脉络其实就一条线基石SCN/Undo/ITL→ 引擎事务如何运转→ 特例DDL 这类结构性动作→ 善后commit 后的块清除→ 旁观别人眼中的一致性读。从事务的诞生到它的终结再到它在别人眼中的样子——事务层这张图的每个角落都点亮了。下次再遇到诡异的性能抖动、莫名其妙的 1555、或者一个怎么都解释不通的查询结果希望你能想起这五块拼图。功底这个东西就是当别人只看到报错的时候你脑子里能自动放映出整条链路的画面
返回列表