
第一部分:官方严谨的详细阐述
一、 核心概念与原子性保证
事务的原子性(Atomicity)要求一个事务的所有操作要么全部提交(永久生效),要么全部回滚(仿佛从未发生过)。Oracle通过其核心的多版本一致性模型(Multi-Version Read Consistency)和一套精巧的Undo/Redo机制来保证这一点。其实现远不止“应用UNDO”那么简单,它是一个贯穿内存结构、磁盘存储、后台进程的协同过程。
二、 核心组件与它们的角色
-
Undo(回滚)数据:
- 作用: 存储事务修改前的数据镜像(前映像,Before Image)。主要用于:
- 事务回滚: 将修改后的数据恢复到事务开始前的状态。
- 读一致性: 为其他查询提供一致性视图,避免读阻塞写。
- 事务恢复: 在实例崩溃后,回滚未提交的事务。
- 存储形式: 存储在Undo表空间的Undo段(Rollback Segment)中。每个Undo段由一个段头块(Header Block)和一系列数据块组成。
- 作用: 存储事务修改前的数据镜像(前映像,Before Image)。主要用于:
-
Redo(重做)日志:
- 作用: 记录对数据库所做的所有物理更改(包括对Data Block和Undo Block的更改)。主要用于:
- 保证事务的持久性(Durability): 事务提交时,LGWR进程必须将对应的Redo记录写入磁盘Redo日志文件。
- 实例恢复: 在实例崩溃后,通过重放Redo日志,将数据库恢复到崩溃前的状态(包括已提交和未提交的事务)。
- 关键点: 对Undo Block的更改也会产生Redo记录。这是实现崩溃恢复的基石。
- 作用: 记录对数据库所做的所有物理更改(包括对Data Block和Undo Block的更改)。主要用于:
-
事务表(Transaction Table):
- 位置: 每个Undo段的段头块中都包含一个事务表。
- 内容: 每个活动事务(未提交的事务)在其使用的Undo段的事务表中占有一个槽位(Slot),记录了关键信息,其中最重要的两个状态位是:
- UBA(Undo Block Address): 指向该事务最近产生的Undo数据地址。
- 状态(Status): 常见状态有
ACTIVE(活跃,未提交)、COMMITTED(已提交)、DEAD(已死,未提交但需要回滚)。
- 作用: 它是事务的“花名册”,系统通过它来快速定位和管理所有未完成的事务。
-
系统更改号(SCN - System Change Number):
- 作用: Oracle内部的逻辑时间戳,用于对数据库的所有更改进行排序。提交事务时,事务会被标记上一个唯一的SCN。SCN是协调数据块、Undo段、Redo日志的核心序列号。
-
后台进程:
- 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时,其“精确步骤”如下:- 生成SCN: LGWR从SCN生成器获取一个唯一的SCN号用于本事务。
- 刷新Redo(保证持久性): LGWR将Redo日志缓冲区中包括本次事务所有更改(含Undo更改)的Redo记录,立即写入磁盘的Redo日志文件。这是提交过程中唯一需要等待的物理I/O操作。一旦写入成功,即使此时实例崩溃,事务也已持久化,因为Redo中已记录全部信息。
- 提交清理(Commit Cleanup): Server Process会找到该事务在Undo段头事务表中对应的槽位,将其状态从
ACTIVE更改为COMMITTED,并写入提交SCN。这个“更改事务表状态”的动作本身也会产生Redo记录。 - 释放锁: 释放事务持有的所有锁(行锁、表锁等)。
- 返回成功: 向用户会话返回“提交成功”信息。
注意: 提交并不会立即清除Undo数据或立即将数据块写回磁盘(由DBWR延迟写入)。提交只是将事务状态标记为完成。
2. 正常回滚(Rollback)
- 当用户发出
ROLLBACK时,Server Process会:- 根据数据块ITL或Undo段头事务表找到该事务的UBA。
- 顺着Undo记录链,从最新的更改开始,逐一读取前映像数据,逆向恢复数据块。
- 在回滚过程中,这些“恢复数据块”的操作同样会生成Redo记录,以保证回滚操作本身的可恢复性。
- 回滚完成后,在事务表中将事务状态标记为已结束(可能变为空槽),并释放锁。
3. 实例崩溃后的事务恢复(SMON的角色)
这是最复杂和精妙的部分。实例崩溃时,内存中的数据(包括未写盘的数据块和事务状态)全部丢失。磁盘上处于一个“不一致”状态:可能包含了未提交事务的更改(因为DBWR异步写盘),也可能丢失了已提交事务的更改(因为更改还在缓冲区没写盘)。
SMON在数据库下次启动时,会自动执行实例恢复,分为两个阶段:
-
前滚(Roll Forward):
- 做什么: SMON从头读取当前的Redo日志文件,重放(replay)其中所有的更改(包括对数据块的更改和对Undo块的更改),直到Redo日志的末尾。
- 为什么: 这个操作的目的有两个:
- 确保所有已提交的事务更改(数据)被写到数据文件中(因为提交时可能数据块还没写盘)。
- 重建崩溃瞬间所有活动事务的状态。通过重放对Undo块的更改,SMON完整地重建了所有Undo段头的事务表。此时,事务表的状态与实例崩溃前一刻完全一致:哪些事务是
COMMITTED,哪些是ACTIVE,一清二楚。同时,Undo数据也被恢复。
- 结果: 此时,数据库包含了所有已提交和未提交的更改,时间点回到了崩溃发生的那一刹那。
-
回滚(Roll Back):
- 做什么: SMON(或有时由Server Process分担)扫描所有Undo段头的事务表,找到所有状态为
ACTIVE(即未提交)的事务。 - 怎么做: 就像执行正常的
ROLLBACK一样,SMON利用前滚阶段已恢复的Undo数据,逐一回滚这些事务。 - 结果: 撤销所有未提交的更改,从而保证事务的原子性:已提交的永久生效,未提交的彻底消失。
- 做什么: SMON(或有时由Server Process分担)扫描所有Undo段头的事务表,找到所有状态为
举例说明恢复过程:
假设有两个并发事务:
- Txn 100:
UPDATE emp SET sal = 1000 WHERE id = 1;并已提交。 - Txn 200:
UPDATE emp SET sal = 2000 WHERE id = 2;但未提交。
此时实例崩溃。DBWR可能只将Txn 200的更改写入了数据文件(因为它的块更“脏”),而Txn 100的更改还在缓冲区。
-
前滚: 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未提交。
- 它重放了Txn 100对数据块和Undo块的更改。同时,也将Txn 100“提交”的记录(事务表状态更改)重放,于是事务表显示Txn 100为
-
回滚: SMON扫描事务表,发现Txn 200是
ACTIVE。于是它找到Txn 200的Undo记录,将id=2的sal值恢复为旧值(比如800)。这个回滚操作也会被记录到Redo中。 -
结果: 数据库恢复到一个一致的状态:id=1的sal是1000(已提交),id=2的sal是800(未提交的更改被回滚)。
四、 争用、等待事件、排查与解决
场景: 高并发OLTP系统中,频繁的小事务提交和回滚。
1. Undo段头(Transaction Table)争用
- 争用原因: 所有事务都需要在Undo段头块的事务表中分配一个槽位。如果并发事务数极高,而Undo段数量不足,大量进程将排队等待访问同一个Undo段头块。
- 等待事件:
enq: TX - allocate ITL entry或buffer busy waits在Undo段头块上。 - 排查:
- 查询
v$waitstat查看undo header类型的等待次数。
SELECT class, count FROM v$waitstat WHERE class = 'undo header';- 查询
v$session_wait或v$active_session_history找到正在等待undo header的会话。
- 查询
- 解决:
- 增加Undo表空间中的Undo段数量: 默认情况下,Oracle会自动管理Undo段(AUM)。但你可以通过手动创建更多的Undo表空间并指定更大的
UNDO_RETENTION值,来间接促使Oracle创建更多Undo段(_SYSSMU$开头的段)。 - (经典方法)增加回滚段数量: 在非常古老的版本或特殊情况下,可以调整
ROLLBACK_SEGMENTS参数,但现在强烈推荐使用AUM。
- 增加Undo表空间中的Undo段数量: 默认情况下,Oracle会自动管理Undo段(AUM)。但你可以通过手动创建更多的Undo表空间并指定更大的
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_event中log file sync的平均等待时间。
SELECT event, time_waited, average_wait FROM v$system_event WHERE event = 'log file sync';- 同时检查
v$system_event中log 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事务机制想象成一个高度严谨的财务做账过程。
-
核心人物:
- 你(Server Process): 业务员,负责处理具体业务(执行DML)。
- 流水账(Redo Log): 一个只追加的流水账簿。每一笔资金的来龙去脉,无论最终是否成功,都按时间顺序记下来。这是公司的生命线。
- 草稿纸/修改记录(Undo Data): 你做账时用的草稿纸。比如要把A账户的100元转到B账户,你会先在草稿纸上写:“A账户原余额500元,B账户原余额200元”。
- 总台账(Data Files): 最终正式的账本。
- 财务总监(SMON): 负责审计和恢复。
- 档案管理员(LGWR): 负责定时把流水账誊写到永久档案(磁盘)中。
-
正常做业务(执行DML):
- 你接到指令:给员工加工资。
- 你先翻看草稿纸,记录下他现在的工资是多少(生成Undo)。
- 然后你在流水账上记两笔:“在草稿纸X页记下了旧工资”和“把总台账中他的工资改为新数额”(生成Redo)。
- 最后你再去总台账上修改他的工资(修改Data Block)。
-
确认业务(提交Commit):
- 老板说:“这个加薪没问题,生效!”
- 你立刻找到档案管理员(LGWR),让他把你刚才在流水账上关于加薪的那几笔记录立刻、马上、永久地归档到档案库(写入磁盘Redo Log文件)。这是你最紧张的一步,必须等他归档完你才能放松。
- 管理员归档完成后,你在你的“待办事项列表”(事务表)上,给这个加薪任务画个勾,标记为“已完成”。
- 然后你对老板说:“搞定!”。
-
发现做错,撤销业务(回滚Rollback):
- 老板说:“加错人了!快改回去!”
- 你不慌不忙地拿出你的草稿纸(Undo),找到记录旧工资的那一页。
- 然后你在流水账(Redo) 上再记一笔:“根据草稿纸X页,把总台账中的工资恢复为旧值”。(回滚也产生Redo!)
- 最后你去总台账上把数字改回去。
- 在你的“待办事项列表”上把这个任务划掉。
-
突然停电(实例崩溃):
- 突然办公室停电,所有人脑子里的记忆(内存)都清空了。
- 来电后,财务总监(SMON) 出场。
- 第一步:补流水账(前滚): 他把档案库里最新的流水账全部拿出来,从昨天关账的地方开始,不管对错,把所有业务一笔一笔地重新做到总台账里。这样,总台账就恢复到了停电前最后一刻的样子,包括那些老板还没确认(未提交)的更改。
- 第二步:核对确认清单(回滚): 总监现在开始核对“待办事项列表”(事务表,这个列表的信息在补流水账的过程中也被恢复了)。他发现有些业务老板还没确认(状态是ACTIVE)。于是,他根据草稿纸(Undo数据),把这些业务的影响全部反向操作一遍,并在流水账上做好记录。
- 最终: 总台账里只剩下老板确认过的业务。整个账本恢复了完整性和一致性。
争用是什么?
- Undo段头争用:就像全公司只有一张“待办事项列表”,所有人都要挤在这张表上写自己的任务进度,自然要排队。
- 提交延迟:就像档案管理员只有一个,业务确认都要等他来归档,他忙不过来或者档案室太远(I/O慢),大家就都得等着。
希望这个结合了技术细节和通俗比喻的解释能让你对Oracle的事务恢复与回滚机制有一个全面而深入的理解。
欢迎关注我的公众号《IT小Chen》
456

被折叠的 条评论
为什么被折叠?



