Oracle 事务恢复与回滚的原子性是如何实现的?

在这里插入图片描述


第一部分:官方严谨的详细阐述

一、 核心概念与原子性保证

事务的原子性(Atomicity)要求一个事务的所有操作要么全部提交(永久生效),要么全部回滚(仿佛从未发生过)。Oracle通过其核心的多版本一致性模型(Multi-Version Read Consistency)和一套精巧的Undo/Redo机制来保证这一点。其实现远不止“应用UNDO”那么简单,它是一个贯穿内存结构、磁盘存储、后台进程的协同过程。

二、 核心组件与它们的角色
  1. Undo(回滚)数据

    • 作用: 存储事务修改前的数据镜像(前映像,Before Image)。主要用于:
      • 事务回滚: 将修改后的数据恢复到事务开始前的状态。
      • 读一致性: 为其他查询提供一致性视图,避免读阻塞写。
      • 事务恢复: 在实例崩溃后,回滚未提交的事务。
    • 存储形式: 存储在Undo表空间的Undo段(Rollback Segment)中。每个Undo段由一个段头块(Header Block)和一系列数据块组成。
  2. Redo(重做)日志

    • 作用: 记录对数据库所做的所有物理更改(包括对Data Block和Undo Block的更改)。主要用于:
      • 保证事务的持久性(Durability): 事务提交时,LGWR进程必须将对应的Redo记录写入磁盘Redo日志文件。
      • 实例恢复: 在实例崩溃后,通过重放Redo日志,将数据库恢复到崩溃前的状态(包括已提交和未提交的事务)。
    • 关键点对Undo Block的更改也会产生Redo记录。这是实现崩溃恢复的基石。
  3. 事务表(Transaction Table)

    • 位置: 每个Undo段的段头块中都包含一个事务表。
    • 内容: 每个活动事务(未提交的事务)在其使用的Undo段的事务表中占有一个槽位(Slot),记录了关键信息,其中最重要的两个状态位是:
      • UBA(Undo Block Address): 指向该事务最近产生的Undo数据地址。
      • 状态(Status): 常见状态有 ACTIVE(活跃,未提交)、COMMITTED(已提交)、DEAD(已死,未提交但需要回滚)。
    • 作用: 它是事务的“花名册”,系统通过它来快速定位和管理所有未完成的事务。
  4. 系统更改号(SCN - System Change Number)

    • 作用: Oracle内部的逻辑时间戳,用于对数据库的所有更改进行排序。提交事务时,事务会被标记上一个唯一的SCN。SCN是协调数据块、Undo段、Redo日志的核心序列号。
  5. 后台进程

    • SMON(System Monitor): 负责实例恢复,包括执行崩溃后的事务恢复(前滚和回滚)。
    • Server Process: 代表用户会话工作的进程,负责构建Undo和Redo记录,并在事务提交时执行“提交清理”。
    • LGWR(Log Writer): 将Redo日志缓冲区的内容写入Redo日志文件。
三、 事务的生命周期与内部原理
1. 正常事务执行与提交(Commit Cleanup)
  • DML操作: 当执行INSERT/UPDATE/DELETE时:

    • Server Process修改数据块,并在修改,将旧数据(前映像)写入Undo段。
    • 对数据块的修改对Undo块的修改都会生成Redo记录,存入Redo日志缓冲区。
    • 数据块上的事务槽(ITL Entry) 会记录该事务的ID(XID)和使用的Undo段地址。
  • 提交(Commit): 当用户发出COMMIT时,其“精确步骤”如下:

    1. 生成SCN: LGWR从SCN生成器获取一个唯一的SCN号用于本事务。
    2. 刷新Redo(保证持久性): LGWR将Redo日志缓冲区中包括本次事务所有更改(含Undo更改)的Redo记录,立即写入磁盘的Redo日志文件。这是提交过程中唯一需要等待的物理I/O操作。一旦写入成功,即使此时实例崩溃,事务也已持久化,因为Redo中已记录全部信息。
    3. 提交清理(Commit Cleanup): Server Process会找到该事务在Undo段头事务表中对应的槽位,将其状态从 ACTIVE 更改为 COMMITTED,并写入提交SCN。这个“更改事务表状态”的动作本身也会产生Redo记录
    4. 释放锁: 释放事务持有的所有锁(行锁、表锁等)。
    5. 返回成功: 向用户会话返回“提交成功”信息。

    注意: 提交并不会立即清除Undo数据或立即将数据块写回磁盘(由DBWR延迟写入)。提交只是将事务状态标记为完成。

2. 正常回滚(Rollback)
  • 当用户发出ROLLBACK时,Server Process会:
    1. 根据数据块ITL或Undo段头事务表找到该事务的UBA。
    2. 顺着Undo记录链,从最新的更改开始,逐一读取前映像数据,逆向恢复数据块。
    3. 在回滚过程中,这些“恢复数据块”的操作同样会生成Redo记录,以保证回滚操作本身的可恢复性。
    4. 回滚完成后,在事务表中将事务状态标记为已结束(可能变为空槽),并释放锁。
3. 实例崩溃后的事务恢复(SMON的角色)

这是最复杂和精妙的部分。实例崩溃时,内存中的数据(包括未写盘的数据块和事务状态)全部丢失。磁盘上处于一个“不一致”状态:可能包含了未提交事务的更改(因为DBWR异步写盘),也可能丢失了已提交事务的更改(因为更改还在缓冲区没写盘)。

SMON在数据库下次启动时,会自动执行实例恢复,分为两个阶段:

  • 前滚(Roll Forward)

    • 做什么: SMON从头读取当前的Redo日志文件,重放(replay)其中所有的更改(包括对数据块的更改和对Undo块的更改),直到Redo日志的末尾。
    • 为什么: 这个操作的目的有两个:
      1. 确保所有已提交的事务更改(数据)被写到数据文件中(因为提交时可能数据块还没写盘)。
      2. 重建崩溃瞬间所有活动事务的状态。通过重放对Undo块的更改,SMON完整地重建了所有Undo段头的事务表。此时,事务表的状态与实例崩溃前一刻完全一致:哪些事务是COMMITTED,哪些是ACTIVE,一清二楚。同时,Undo数据也被恢复。
    • 结果: 此时,数据库包含了所有已提交和未提交的更改,时间点回到了崩溃发生的那一刹那。
  • 回滚(Roll Back)

    • 做什么: SMON(或有时由Server Process分担)扫描所有Undo段头的事务表,找到所有状态为 ACTIVE(即未提交)的事务。
    • 怎么做: 就像执行正常的ROLLBACK一样,SMON利用前滚阶段已恢复的Undo数据,逐一回滚这些事务。
    • 结果: 撤销所有未提交的更改,从而保证事务的原子性:已提交的永久生效,未提交的彻底消失。

举例说明恢复过程
假设有两个并发事务:

  • Txn 100UPDATE emp SET sal = 1000 WHERE id = 1; 并已提交
  • Txn 200UPDATE emp SET sal = 2000 WHERE id = 2; 但未提交

此时实例崩溃。DBWR可能只将Txn 200的更改写入了数据文件(因为它的块更“脏”),而Txn 100的更改还在缓冲区。

  1. 前滚: SMON重放Redo日志。

    • 它重放了Txn 100对数据块和Undo块的更改。同时,也将Txn 100“提交”的记录(事务表状态更改)重放,于是事务表显示Txn 100为COMMITTED
    • 它同样重放了Txn 200对数据块和Undo块的更改。事务表显示Txn 200为ACTIVE
    • 现在,数据文件中id=1和id=2的记录sal都是新值(1000和2000),但我们知道Txn 100已提交,Txn 200未提交。
  2. 回滚: SMON扫描事务表,发现Txn 200是ACTIVE。于是它找到Txn 200的Undo记录,将id=2的sal值恢复为旧值(比如800)。这个回滚操作也会被记录到Redo中

  3. 结果: 数据库恢复到一个一致的状态:id=1的sal是1000(已提交),id=2的sal是800(未提交的更改被回滚)。

四、 争用、等待事件、排查与解决

场景: 高并发OLTP系统中,频繁的小事务提交和回滚。

1. Undo段头(Transaction Table)争用
  • 争用原因: 所有事务都需要在Undo段头块的事务表中分配一个槽位。如果并发事务数极高,而Undo段数量不足,大量进程将排队等待访问同一个Undo段头块。
  • 等待事件enq: TX - allocate ITL entrybuffer busy waits 在Undo段头块上。
  • 排查
    • 查询 v$waitstat 查看undo header类型的等待次数。
    SELECT class, count FROM v$waitstat WHERE class = 'undo header';
    
    • 查询 v$session_waitv$active_session_history 找到正在等待undo header的会话。
  • 解决
    • 增加Undo表空间中的Undo段数量: 默认情况下,Oracle会自动管理Undo段(AUM)。但你可以通过手动创建更多的Undo表空间并指定更大的UNDO_RETENTION值,来间接促使Oracle创建更多Undo段(_SYSSMU$开头的段)。
    • (经典方法)增加回滚段数量: 在非常古老的版本或特殊情况下,可以调整ROLLBACK_SEGMENTS参数,但现在强烈推荐使用AUM。
2. Undo数据块争用
  • 争用原因: 多个事务试图同时向同一个Undo段中的Undo数据块写入数据。
  • 等待事件buffer busy waits 在Undo数据块上。
  • 排查v$waitstat 中的undo block等待计数。
    SELECT class, count FROM v$waitstat WHERE class = 'undo block';
    
  • 解决
    • 增加Undo表空间大小: 提供更多的Undo空间,使得事务可以写入不同的Extents。
    • 增加更多的Undo段: 更多段意味着事务更分散,减少对同一组数据块的竞争。
    • 优化程序: 减少单个事务的大小和长度。
3. 提交延迟(Log File Sync)
  • 争用原因: 提交时,必须等待LGWR将Redo日志缓冲区同步写入磁盘。如果I/O子系统慢,或日志文件所在磁盘繁忙,所有提交操作都将被卡住。
  • 等待事件log file sync (这是用户会话在等待的事件)
  • 排查
    • 查看 v$system_eventlog file sync的平均等待时间。
    SELECT event, time_waited, average_wait FROM v$system_event
    WHERE event = 'log file sync';
    
    • 同时检查 v$system_eventlog file parallel write的平均等待时间(这是LGWR进程在等待的事件)。如果它也很高,说明是I/O问题。
  • 解决
    • 使用更快的I/O设备: 将Redo日志文件放在高速磁盘(如SSD)上,并与数据文件等其他文件隔离。
    • 调整日志文件大小和组数: 避免日志切换过于频繁。增大日志文件大小,但注意恢复时间。
    • 启用异步提交COMMIT WRITE NOWAIT): 在12c+,可以牺牲一定的持久性来换取性能,但一般不推荐。
五、 常用管理SQL语句
-- 1. 查看当前活动事务信息 (需要访问V$TRANSACTION和V$SESSION)
SELECT s.sid, s.serial#, s.username, t.start_time, t.status, t.used_ublk, t.used_urec
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr;

-- 2. 查看Undo表空间使用情况
SELECT tablespace_name, status, sum(bytes)/1024/1024 size_mb
FROM dba_undo_extents
GROUP BY tablespace_name, status;

-- 3. 查看Undo段统计信息
SELECT usn, extents, writes, gets, waits
FROM v$rollstat;

-- 4. 查看等待事件(实时)
SELECT sid, event, wait_time, seconds_in_wait
FROM v$session_wait
WHERE event NOT LIKE '%SQL%Net%' AND event NOT LIKE 'rdbms%';

-- 5. 查看系统级等待事件统计
SELECT event, total_waits, time_waited, average_wait
FROM v$system_event
WHERE wait_class <> 'Idle'
ORDER BY time_waited DESC;

-- 6. 检查正在回滚的长事务(used_ublk很高且status是ACTIVE的)
SELECT s.sid, s.serial#, t.start_time, t.used_ublk, t.used_urec
FROM v$transaction t, v$session s
WHERE t.addr = s.taddr
AND t.status = 'ACTIVE'
AND t.used_ublk > 10000; -- 例如,回滚块数大于10000

第二部分:通俗易懂的解释

可以把整个Oracle事务机制想象成一个高度严谨的财务做账过程

  1. 核心人物

    • 你(Server Process): 业务员,负责处理具体业务(执行DML)。
    • 流水账(Redo Log): 一个只追加的流水账簿。每一笔资金的来龙去脉,无论最终是否成功,都按时间顺序记下来。这是公司的生命线
    • 草稿纸/修改记录(Undo Data): 你做账时用的草稿纸。比如要把A账户的100元转到B账户,你会先在草稿纸上写:“A账户原余额500元,B账户原余额200元”。
    • 总台账(Data Files): 最终正式的账本。
    • 财务总监(SMON): 负责审计和恢复。
    • 档案管理员(LGWR): 负责定时把流水账誊写到永久档案(磁盘)中。
  2. 正常做业务(执行DML)

    • 你接到指令:给员工加工资。
    • 先翻看草稿纸,记录下他现在的工资是多少(生成Undo)。
    • 然后你在流水账上记两笔:“在草稿纸X页记下了旧工资”和“把总台账中他的工资改为新数额”(生成Redo)。
    • 最后你再去总台账上修改他的工资(修改Data Block)。
  3. 确认业务(提交Commit)

    • 老板说:“这个加薪没问题,生效!”
    • 你立刻找到档案管理员(LGWR),让他把你刚才在流水账上关于加薪的那几笔记录立刻、马上、永久地归档到档案库(写入磁盘Redo Log文件)。这是你最紧张的一步,必须等他归档完你才能放松
    • 管理员归档完成后,你在你的“待办事项列表”(事务表)上,给这个加薪任务画个勾,标记为“已完成”。
    • 然后你对老板说:“搞定!”。
  4. 发现做错,撤销业务(回滚Rollback)

    • 老板说:“加错人了!快改回去!”
    • 你不慌不忙地拿出你的草稿纸(Undo),找到记录旧工资的那一页。
    • 然后你在流水账(Redo) 上再记一笔:“根据草稿纸X页,把总台账中的工资恢复为旧值”。(回滚也产生Redo!)
    • 最后你去总台账上把数字改回去。
    • 在你的“待办事项列表”上把这个任务划掉。
  5. 突然停电(实例崩溃)

    • 突然办公室停电,所有人脑子里的记忆(内存)都清空了。
    • 来电后,财务总监(SMON) 出场。
    • 第一步:补流水账(前滚): 他把档案库里最新的流水账全部拿出来,从昨天关账的地方开始,不管对错,把所有业务一笔一笔地重新做到总台账里。这样,总台账就恢复到了停电前最后一刻的样子,包括那些老板还没确认(未提交)的更改。
    • 第二步:核对确认清单(回滚): 总监现在开始核对“待办事项列表”(事务表,这个列表的信息在补流水账的过程中也被恢复了)。他发现有些业务老板还没确认(状态是ACTIVE)。于是,他根据草稿纸(Undo数据),把这些业务的影响全部反向操作一遍,并在流水账上做好记录。
    • 最终: 总台账里只剩下老板确认过的业务。整个账本恢复了完整性和一致性

争用是什么?

  • Undo段头争用:就像全公司只有一张“待办事项列表”,所有人都要挤在这张表上写自己的任务进度,自然要排队。
  • 提交延迟:就像档案管理员只有一个,业务确认都要等他来归档,他忙不过来或者档案室太远(I/O慢),大家就都得等着。

希望这个结合了技术细节和通俗比喻的解释能让你对Oracle的事务恢复与回滚机制有一个全面而深入的理解。

欢迎关注我的公众号《IT小Chen

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值