本节摘要:SQL 标准定义了四档隔离级别,Oracle 只实现其中两档——读已提交与串行化——却用多版本一致读把"脏读"从根源上消灭了。本节讲清每档防住什么、放进来什么,Oracle 一致读的实现代价,以及为什么报表与交易可以共用一张表而不互相等待。
并发读写会产生几类经典异常:脏读(读到别人还没提交、可能回滚的数据)、不可重复读(同一事务里两次读同一行结果不同,因为中间被人提交了修改)、幻读(同一条件两次查询行数不同,中间被插入了新行)。隔离级别就是"你愿意容忍哪些异常"的档位表:读未提交容忍全部(脏读都有);读已提交消灭脏读;可重复读再消灭不可重复读;串行化全部消灭。档位越高,异常越少,但并发代价越高——要么等待变多,要么代价转移到别处。
标准是标准,实现是另一回事。Oracle 只做了两档:读已提交(默认)与串行化,外加一个独家的"只读事务"。更有意思的是实现方式——别的数据库常用"读加锁"来保证可重复读,Oracle 用"读旧版本":所有查询拿一个时间戳,看到的数据一律是该时刻已提交的版本,别人改了没关系,去 UNDO 翻旧账。于是脏读在 Oracle 里根本不可能发生,连读未提交档位都没有——它在物理层面就无处实现。
**读已提交(READ COMMITTED)**是默认档:每条语句开始时取一个快照,语句内一致,语句间可能变化。它防住脏读,容忍不可重复读与幻读。绝大多数 OLTP 场景的正确选择——等待少、并发高,应用通过"关键处再查一次"或乐观校验处理偶尔的读变化。
串行化(SERIALIZABLE):事务开始时取快照,整个事务内读到的世界冻结在那一刻。防住不可重复读与幻读。代价有两层:其一,快照变长,UNDO 翻旧账的距离变远,ORA-01555 风险上升;其二,两个并发事务若修改了彼此读过的数据,后提交者报 ORA-08177 无法串行化访问——应用必须准备重试逻辑。适合"读一大片、算一个结论、再写回去"的对账类事务,且事务要短。
**只读事务(READ ONLY)**是 Oracle 独家:事务内只读且整体快照一致,等价于"串行化减去写"。月末出报表、跨多张表取数的场景最合适——一张冻结的时间快照,不怕跑批中途有人插数据。
| 档位 | 快照粒度 | 防住 | 放进来 | 典型场景 |
|---|---|---|---|---|
| 读已提交 | 每语句一个 | 脏读 | 不可重复读、幻读 | OLTP 默认选择 |
| 串行化 | 每事务一个 | 脏读、不可重复读、幻读 | ORA-08177 需重试 | 对账、结算类短事务 |
| 只读事务 | 每事务一个(禁写) | 全部读异常 | 无写能力 | 报表、取数快照 |
-- 三种档位的开启方式 ALTER SESSION SET isolation_level = READ COMMITTED; -- 默认,通常不用显式设 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 事务开头声明串行化 SET TRANSACTION READ ONLY; -- 只读快照,报表专用 -- 验证一致读的效果:事务里两次读同一张表 -- 串行化下两次结果完全一致;读已提交下第二次可能看到别人提交的新行
背景。 月末 22:00,财务同时跑两件事:跑批程序按日累加更新汇总表,报表程序统计全月明细。次日凌晨对不上:报表上的月合计比汇总表多了 41 笔——正是 22 点后新入库的明细。财务怀疑报表算错,报表工程师反复核对 SQL 无果。
操作。 复盘先确认双方隔离级别:报表走默认读已提交,统计执行了 40 分钟,期间跑批不断提交新明细——报表的分时段统计跨越了别人的提交点,前半段统计看到 22 点前的世界,后半段看到了 22:15 的世界,中间那 41 笔被算进月合计。这不是 bug,是读已提交的精确定义:语句级一致,跨语句无承诺。
结果。 处置只改一行:报表入口加 SET TRANSACTION READ ONLY,整个报表冻结在开始时刻的快照。次日复跑,与汇总表差额归零。同时给报表程序约定了 UNDO 侧的配合——这是同一次复盘的第二产出。
解读。 案例的三层收获:其一,"对不上"不等于"算错了",先核对隔离级别的语义边界再查 SQL;其二,Oracle 的只读事务是把"一致性快照"当产品交付,报表业务的正确姿势不是改 SQL 而是选对档位;其三,快照不是免费的——40 分钟的一致读需要 40 分钟前能翻到的 UNDO,UNDO 保留不足就报 01555,报表照样失败。变式。 若报表改到只读事务后又报 01555,正确顺序是:查该时段 UNDO 消耗速率,把 UNDO 表空间按"最长查询时长乘以峰值变更速率"扩容,或把报表安排在变更低谷——而不是简单调大 UNDO_RETENTION 参数去硬顶磁盘上限。
从 MySQL 转来的工程师常问:Oracle 为什么没有可重复读?答案藏在实现差异里。InnoDB 的可重复读用 MVCC 加间隙锁:读不加锁靠版本,但要防幻读时对扫描范围加锁,锁住了"别人插入的自由"。Oracle 的选择是不提供事务级防幻读的普通档位——要防就上串行化,用乐观冲突检测(ORA-08177)代替间隙锁。两种路线各有代价:InnoDB 把代价付在锁竞争,Oracle 把代价付在重试概率。理解这一点,跨库迁移时的隔离级别评估才有坐标系:不是"有没有这一档",而是"防幻读的成本由谁承担"。
⚠️ 常见坑:在串行化事务里做长报表。串行化的快照同样会 01555,且写冲突重试在高并发下会雪崩。串行化留给短的对账事务,长报表交给只读事务——两者都靠快照,脾气却不同。
问题一:怎么验证应用在哪个隔离级别跑? 会话里查一下事务特性最直接;更实用的是行为验证——在测试环境开两个会话,一个事务里两次读同一张表、另一个会话在中间插入提交,两次读结果相同即事务级快照(串行化或只读),不同即语句级快照(读已提交)。行为证据比配置检查更可信,因为框架可能在运行时改档位。
问题二:ORA-01555 已经发生了怎么应急? 短期先救查询:把报表拆成更短的批次(缩小需要的快照窗口),或用闪回查询锁定一个明确时刻。中期补容量:按"最长查询时长乘峰值变更速率"扩 UNDO 并固定大小。长期改机制:超长一致性读的需求(几小时的报表)别再依赖在线 UNDO——改跑在备库、或落地为定时物化的中间结果,让 UNDO 只服务分钟级的一致读。
问题三:串行化的重试该怎么实现才不雪崩? 重试必须带退避与上限:捕获 ORA-08177 后随机等待再重试,超过三次放弃并告警。无退避的重试风暴会把冲突方锁死在同一节奏上互踩。更结构化的解法是把冲突检测前置——事务开头把要改的行先做一次预检查加标记,冲突方提前失败,减少走到提交才发现的浪费。
本节要点回顾
第 3 章到此解决了"改得对、看得准"。下一章进入 SQL 引擎:一条语句怎么被解析、优化、执行,执行计划为什么是慢查询的唯一口供。