`
linsea
  • 浏览: 87919 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Redo 与 Undo (1)

阅读更多

通常对undo有一个误解,认为undo用于数据库物理地恢复到执行语句或事务之前的样子,但实际上并非如此。数据库只是逻辑地恢复到原来的样子,所有修改都被逻辑地取消,但是数据结构以及数据库块本身在回滚后可能大不相同。原因在于:在所有多用户系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要功能之一就是协调对数据的并发访问。也许我们的事务在修改一些块,而一般来讲往往会有许多其他的事务也在修改这些块。因此,不能简单地将一个块放回到我们的事务开始前的样子,这样会撤销其他人(其他事务)的工作!

   

9.3 redoundo如何协作?

有意思的是,尽管undo信息存储在undo表空间或undo段中,但也会受到redo的保护。换句话说,会把undo数据当成是表数据或索引数据一样,对undo的修改会生成一些redo,这些redo将计入日志。为什么会这样呢?稍后在讨论系统崩溃时发生的情况时将会解释它,到时你会明白了。将undo数据增加到undo段中,并像其他部分的数据一样在缓冲区缓存中得到缓存。另外这些redo信息还用于在实例恢复时重建SGA内存的状态。

 

9.3.1 COMMIT做什么?

COMMIT通常是一个非常快的操作,而不论事务大小如何。你可能认为,一个事务越大(换句话说,它影响的数据越多),COMMIT需要的时间就越长。不是这样的。不论事务有多大,COMMIT的响应时间一般都很“平”(flat,可以理解为无高低变化)。这是因为COMMIT并没有太多的工作去做,不过它所做的确实至关重要。

这一点很重要,之所以要了解并掌握这个事实,原因之一是:这样你就能心无芥蒂地让事务有足够的大小。一种错误的信念认为分批提交可以节省稀有的系统资源,而实际上这只是增加了资源的使用。如果只在必要时才提交(即逻辑工作单元结束时),不仅能提高性能,还能减少对共享资源的竞争(日志文件、各种内部闩等)。

分批提交COMMIT的开销存在两个因素:

l         显然会增加与数据库的往返通信。如果每个记录都提交,生成的往返通信量就会大得多。

l         每次提交时,必须等待redo写至磁盘。这会导致“等待”。在这种情况下,等待称为“日志文件同步”(log file sync)。

 

为什么COMMIT的响应时间相当“平”,而不论事务大小呢?在数据库中执行COMMIT之前,困难的工作都已经做了。我们已经修改了数据库中的数据,所以99.9%的工作都已经完成。例如,已经发生了以下操作:

l         已经在SGA中生成了undo块。

l         已经在SGA中生成了已修改数据块。

l         已经在SGA中生成了对于前两项的缓存redo

l         取决于前三项的大小,以及这些工作花费的时间,前面的每个数据(或某些数据)可能已经刷新输出到磁盘。

l         已经得到了所需的全部锁。

执行COMMIT时,余下的工作只是:

l         为事务生成一个SCN。如果你还不熟悉SCN,起码要知道,SCNOracle使用的一种简单的计时机制,用于保证事务的顺序,并支持失败恢复。SCN还用于保证数据库中的读一致性和检查点。可以把SCN看作一个钟摆,每次有人COMMIT时,SCN都会增1.

l         LGWR将所有余下的缓存重做日志条目写到磁盘,并把SCN记录到在线重做日志文件中。这一步就是真正的COMMIT。如果出现了这一步,即已经提交。事务条目会从V$TRANSACTION中“删除”,这说明我们已经提交。

l         V$LOCK中记录这我们的会话持有的锁,这些所都将被释放,而排队等待这些锁的每一个人都会被唤醒,可以继续完成他们的工作。

l         如果事务修改的某些块还在缓冲区缓存中,则会以一种快速的模式访问并“清理”。块清除(Block cleanout)是指清除存储在数据库块首部的与锁相关的信息。实质上讲,我们在清除块上的事务信息,这样下一个访问这个块的人就不用再这么做了。我们采用一种无需生成重做日志信息的方式来完成块清除,这样可以省去以后的大量工作(在下面的“块清除”一节中将更全面地讨论这个问题)。

 

可以看到,处理COMMIT所要做的工作很少。其中耗时最长的操作要算LGWR执行的活动(一般是这样),因为这些磁盘写是物理磁盘I/O不过,这里LGWR花费的时间并不会太多,之所以能大幅减少这个操作的时间,原因是LGWR一直在以连续的方式刷新输出重做日志缓冲区的内容。在你工作期间,LGWR并非缓存这你做的所有工作;实际上,随着你的工作的进行,LGWR会在后台增量式地刷新输出重做日志缓冲区的内容。这样做是为了避免COMMIT等待很长时间来一次性刷新输出所有的redo

因此,即使我们有一个长时间运行的事务,但在提交之前,它生成的许多缓存重做日志已经刷新输出到磁盘了(而不是全部等到提交时才刷新输出)。这也有不好的一面,COMMIT时,我们必须等待,直到尚未写出的所有缓存redo都已经安全写到磁盘上才行。也就是说,对LGWR的调用是一个同步(synchronous)调用。尽管LGWR本身可以使用异步I/O并行地写至日志文件,但是我们的事务会一直等待LGWR完成所有写操作,并收到数据都已在磁盘上的确认才会返回。

前面我提高过,由于某种原因,我们用的是一个Java程序而不是PL/SQL,这个原因就是PL/SQL提供了提交时优化(commit-time optimization。我说过,LGWR是一个同步调用,我们要等待它完成所有写操作。在Oracle 10g Release 1及以前版本中,除PL/SQL以外的所有编程语言都是如此。PL/SQL引擎不同,要认识到直到PL/SQL例程完成之前,客户并不知道这个PL/SQL例程中是否发生了COMMIT,所以PL/SQL引擎完成的是异步提交。它不会等待LGWR完成;相反,PL/SQL引擎会从COMMIT调用立即返回。不过,等到PL/SQL例程完成,我们从数据库返回客户时,PL/SQL例程则要等待LGWR完成所有尚未完成的COMMIT因此,如果在PL/SQL中提交了100次,然后返回客户,会发现由于存在这种优化,你只会等待LGWR一次,而不是100次。这是不是说可以在PL/SQL中频繁地提交呢?这是一个很好或者不错的主意吗?不是,绝对不是,在PL/SQ;中频繁地提交与在其他语言中这样做同样糟糕。指导原则是,应该在逻辑工作单元完成时才提交,而不要在此之前草率地提交。

    COMMIT是一个“响应时间很平”的操作,虽然不同的操作将生成不同大小的redo,即使大小相差很大或者说无论生成多少redo,但也并不会影响提交(COMMIT)的时间或者说提交所用的时间都基本相同。

 

9.3.2 ROLLBACK做什么?

一般地回滚时间是所修改数据量的一个函数。回滚操作的开销很大,这是可以想像的,因为ROLLBACK必须物理地撤销我们所做的工作。类似于COMMIT,必须完成一系列操作。在到达ROLLBACK之前,数据库已经做了大量的工作。再复习一遍,可能已经发生的操作如下:

l         已经在SGA中生成了undo块。

l         已经在SGA中生成了已修改数据块。

l         已经在SGA中生成了对于前两项的缓存redo

l         取决于前三项的大小,以及这些工作花费的时间,前面的每个数据(或某些数据)可能已经刷新输出到磁盘。

l         已经得到了所需的全部锁。

 

ROLLBACK时,要做以下工作:

l         撤销已做的所有修改。其完成方式如下:从undo段读回数据,然后实际上逆向执行前面所做的操作,并将undo条目标记为已用。如果先前插入了一行,ROLLBACK会将其删除。如果更新了一行,回滚就会取消更新。如果删除了一行,回滚将把它再次插入。

l         会话持有的所有锁都将释放,如果有人在排队等待我们持有的锁,就会被唤醒。

 

1.4   分析redo

作为一名开发人员,应该能够测量你的操作生成了多少redo,这往往很重要。生成的redo越多,你的操作花费的时间就越长,整个系统也会越慢。你不光在影响你自己的会话,还会影响每一个会话。redo管理是数据库中的一个串行点。任何Oracle实例都只有一个LGWR,最终所有事务都会归于LGWR,要求这个进程管理它们的redo,并BOMMIT其事务,LGWR要做的越多,系统就会越慢。

 

9.4.1 测量redo

要查看生成的redo量相当简单,这在本章前面已经见过。我使用了SQL*Plus的内置特性AUTOTRACE。不过AUTOTRACE只能用于简单的DML,对其他操作就力所不能及了,例如,它无法查看一个存储过程调用做了什么。为此,我们需要访问两个动态性能视图:

l         V$MYSTAT,其中有会话的提交信息。

l         V$STATNAME,这个视图能告诉我们V$MYSTAT中的每一行表示什么(所查看的统计名)。

 

9.4.2 redo生成和BEFORE/AFTER触发器

l         BEFOREAFTER触发器不影响DELETE生成的redo

l         Oracle9i Release 2 及以前版本中,BEFOREAFTER触发器会使INSERT生成同样数量的额外redo。在Oracle 10g中,则不会生成任何额外的redo

l         Oracle9i Release 2及以前的所有版本中,UPDATE生成的redo只受BEFORE触发器的影响。AFTER触发器不会增加任何额外的redo。不过,在Oracle 10g中,情况又有所变化。具体表现为:

Ø         总的来讲,如果一个表没有触发器,对其更新期间生成的redo量总是比Oracle9i及以前版本中要少。看来这是Oracle着力解决的一个关键问题:对于触发器的表,要减少这种表更新所生成的redo量。

Ø         Oracle 10g中,如果表有一个BEFORE触发器,则其更新期间生成的redo量比9i中更大。

Ø         如果表有AFTER触发器,则更新所生成的redo量与9i中一样。

 

Oracle9i Release 2Oracle 10g这两个版本之间,触发器对事务实际生成的redo存在不同的影响。可以很容易地看到这些变化:

l         是否存在触发器对DELETE没有影响(DELETE还是不受触发器的影响)。

l         Oracle9i Release 2及以前版本中,INSERT会受到触发器的影响。初看上去,你可能会说,Oracle 10g优化了INSERT,所以它不会受到影响,但是再看看Oracle 10g中无触发器时生成的redo总量,你会看到,这与Oracle9i Release 2及以前版本中有触发器时生成的redo量是一样的。所以,并不是Oracle 10g减少了有触发器时INSERT生成的redo量,而是所生成的redo量是常量(有无触发器都会生成同样多的redo),无触发器时,Oracle 10g中的INSERTOracle9i中生成的redo要多。

l         9i中,UPDATE会受BEFORE触发器的影响,但不受AFTER触发器的影响。初看上去,似乎Oracle 10g中改成了两个触发器都会影响UPDATE。但是通过进一步的分析,可以看到,实际上Oracle 10g中无触发器是UPDATE生成的redo有所下降,下降的量正是有触发器时UPDATE生成的redo量。所用与9i10gINSERT的情况恰恰相反,与9i相比,没有触发器时Oracle 10gUPDATE生成的redo量会下降。

 

触发器对redo生成的影响

DML操作

AFTER触发器(10g以前)

BEFORE触发器(10g以前)

AFTER触发器(10g

BEFORE触发器(10g

DELETE

不影响

不影响

不影响

不影响

INSERT

增加redo

增加redo

常量redo

常量redo

UPDATE

增加redo

不影响

增加redo

增加redo

 

现在你应该知道怎么来估计redo量,这是每一个开发人员应该具备的能力。你可以:

l         估计你的“事务”大小(你要修改多少数据)。

l         在要修改的数据量基础上再加10%20%的开销,具体增加多大的开销取决于你要修改的行数。修改行越多,增加的开销就越小。

l         对于UPDATE,要把这个估计值加倍。

 

9.4.3 我能关掉重做日志生成吗?

答案很简单:不能。因为重做日志对于数据库至关重要;它不是开销,不是浪费。

 

1. SQL中设置NOLOGGING

有些SQL语句和操作支持使用NOLOGGING子句。这并不是说:这个对象的所有操作在执行时都不生成重做日志,而是说有些特定操作生成的redo会比平常(即不使用NOLOGGING子句时)少得多。

NOARCHIVELOG模式的数据库中,除了数据字典的修改外,CREATE TABLE不会记录日志。(为什么?CREATE TABLE难道不是数据字典操作?)如果你想在NOARCHIVELOG模式的数据库上看到差别,可以把对表TDROP TABLECREATE TABLE换成DROP INDEXCREATE INDEX。默认情况下,不论数据库以何种模式运行,这些操作都会生成日志。

 

关于NOLOGGING操作,需要注意以下几点:

l         事实上,还是会生成一定数量的redo。这些redo的作用是保护数据字典。这是不可避免的。与以前(不使用NOLOGGING)相比,尽管生成的redo量要少多了,但是确实会有一些redo

l         NOLOGGING不能避免所有后续操作生成redo。在前面的例子中,我创建的并非不生成日志的表。只是创建表(CREATE TABLE)这一个操作没有生成日志。所有后续的“正常“操作(如INSERTUPDATEDELETE)还是会生成日志。其他特殊的操作(如使用SQL*Loader的直接路径加载,或使用INSERT /*+ APPEND */语法的直接路径插入)不生成日志(除非你ALTER这个表,再次启用完全的日志模式)。不过,一般来说,应用对这个表执行的操作都会生成日志。

l         在一个ARCHIVELOG模式的数据库上执行NOLOGGING操作后,必须尽快为受影响的数据文件建立一个新的基准备份,从而避免由于介质失败而丢失对这些对象的后续修改。实际上,我们并不会丢失后来做出的修改,因为这些修改确实在重做日志中;我们真正丢失的只是要应用这些修改的数据(即最初的数据)。

 

2.在索引上设置NOLOGGING

使用NOLOGGING选项有两种方法。你已经看到了前一种,也就是把NOLOGGING关键字潜在SQL命令中。另一种方法是在段(索引或表)上设置NOLOGGING属性,从而隐式地采用NOLOGGING模式来执行操作。

alter一个索引:

ops$tkyte@ORA10G> alter index t_idx rebuild;

Index altered.

以后rebuild的时候只会生成少许日志,而不会生成大量的额外的日志。但是,现在这个索引没有得到保护(unprotected),如果它所在的数据文件失败而必须从一个备份恢复,我们就会丢失这个索引数据。了解这一点很重要。现在索引是不可恢复的,所以需要做一个备份。或者,DBA也可以干脆创建索引,因为完全可以从表数据直接创建索引。

 

3. NOLOGGING小结

可以采用NOLOGGING模式执行以下操作:

l         索引的创建和ALTER(重建)。

l         表的批量INSERT(通过/*+APPEND */提示使用“直接路径插入“。或采用SQL*Loader直接路径加载)。表数据不生成redo,但是所有索引修改会生成redo,但是所有索引修改会生成redo(尽管表不生成日志,但这个表上的索引却会生成redo!)。

l         LOB操作(对大对象的更新不必生成日志)。

l         通过CREATE TABLE AS SELECT创建表。

l         各种ALTER TABLE操作,如MOVESPLIT

在一个ARCHIVELOG模式的数据库上,如果NOLOGGING使用得当,可以加快许多操作的速度,因为它能显著减少生成的重做日志量。

 

9.4.4 为什么不能分配一个新日志?

老是有人问我这个问题。这样做会得到一条警告消息(可以在服务器上的alert.log中看到):

Thread 1 cannot allocate new log, sequence 1466

Checkpoint not complete

Current log# 3 seq# 1465 mem# 0: /home/ora10g/oradata/ora10g/redo03.log

警告消息中也可能指出Archival required而不是Checkpoint not complete,但是效果几乎都一样。DBA必须当心这种情况。如果数据库试图重用一个在线重做日志文件,但是发现做不到,就会把这样一条消息写到服务器上的alert.log中。如果DBWR还没有完成重做日志所保护数据的检查点(checkpointing,或者ARCH还没有把重做日志文件复制到归档目标,就会发生这种情况。对最终用户来说,这个时间点上数据库实际上停止了。它会原

地不动。DBWRARCH将得到最大的优先级以将redo块刷新输出的磁盘。完成了检查点或归档之后,一切又回归正常。

如果你看到会话因为一个“日志文件切换”、“日志缓冲区空间”或“日志文件切换检查点或归档未完成”等待了很长时间,就很可能遇到了这个问题。

要解决这个问题,有几种做法:

 

l         DBWR更快一些。让你的DBADBWR调优,为此可以启用ASYNC I/O、使用DBWR I/O从属进程,或者使用多个DBWR进程。看看系统产生的I/O,查看是否有一个磁盘(或一组磁盘)“太热”,相应地需要将数据散布开。这个建议对ARCH也适用。这种做法的好处是,你不用付出什么代价就能有所收获,性能会提高,而且不必修改任何逻辑/结构/代码。这种方法确实没有缺点。

l         增加更多重做日志文件。在某些情况下,这会延迟Checkpoint not complete的出现,而且过一段时间后,可以把Checkpoint not complete延迟得足够长,使得这个错误可能根本不会出现(因为你给DBWR留出了足够的活动空间来建立检查点)。这个方法也同样适用于Archival required消息。这种方法的好处是可以消除系统中的“暂停”。其缺点是会消耗更多的磁盘空间,但是在此利远远大于弊。

l         重新创建更大的日志文件。这会扩大填写在线重做日志与重用这个在线重做日志文件之间的时间间隔。如果重做日志文件的使用呈“喷射状”,这种方法同样适用于Archival required消息。倘若一段时间内会大量生成日志(如每晚加载、批处理等),其后一段数据却相当平静,如果有更大的在线重做日志,就能让ARCH在平静的期间有足够的时间“赶上来”。这种方法的优缺点与前面增加更多文件的方法是一样的。另外,它可能会延迟检查点的发生,由于(至少)每个日志切换都会发生检查点,而现在日志切换间隔会更大。

l         让检查点发生得更频繁、更连续。可以使用一个更小的块缓冲区缓存(不太好),或者使用诸如FAST_START_MTTR_TARGETLOG_CHECKPOINT_INTERVALLOG_CHECKPOINT_TIMEOUT之类的参数设置。这会强制DBWR更频繁地刷新输出脏块。这种方法的好处是,失败恢复的时间会减少。在线重做日志中应用的工作肯定更少。其缺点是,如果经常修改块,可能会更频繁地写至磁盘。缓冲区缓存本该更有效的,但由于频繁地写磁盘,会导致缓冲区缓存不能充分发挥作用,这可能会影响下一节将讨论的块清除机制。

 

9.4.5 块清除

在第6章中,我们曾经讨论过数据锁以及如何管理它们。我介绍了数据锁实际上是数据的属性,存储在块首部。这就带来一个副作用,下一次访问这个块时,可能必须“清理”这个块,换句话说,要将这些事务信息删除。这个动作会生成redo,并导致变脏(原本并不脏,因为数据本身没有修改),这说明一个简单的SELECT有可能生成redo,而且可能导致完成下一个检查点时将大量的块写至磁盘。不过,在大多数正常的情况下,这是不会发生的。如果系统中主要是小型或中型事务(OLTP),或者数据仓库会执行直接路径加载或使用DBMS_STATS在加载操作后分析表,你会发现块通常已经得到“清理”。如果还记得前面“COMMIT做什么?”一节中介绍的内容,应该知道,COMMIT时处理的步骤之一是:如果块还在SGA中,就要再次访问这些块,如果可以访问(没有别人在修改这些块),则对这些块完成清理。这个 活动称为提交清除(commit cleanout),即清除已修改块上事务信息。最理想的是,COMMIT可以完成块清除,这样后面的SELECT(读)就不必再清理了。只有块的UPDATE才会真正清除残余的事务信息,由于UPDATE已经在生成redo,所用注意不到这个清除工作。

可以强制清除不发生来观察它的副作用,并了解提交清除是怎么工作的。在与我们的事务相关的提交列表中,Oracle会记录已修改的块列表。这些列表都有20个块,Oracle会根据需要分配多个这样的列表,直至达到某个临界点。如果我们修改的块加起来超过了块缓冲区缓存大小的10%Oracle会停止为我们分配新的列表。例如,如果缓冲区缓存设置为可以缓存3,000个块,Oracle会为我们维护最多300个块(3,00010%)。COMMIT时,Oracle会处理这些包含20个块指针的列表,如果块仍可用,它会执行一个很快的清理。所以,只要我们修改的块数没有超过缓存中总块数的10%,而且块仍在缓存中并且是可用的,Oracle就会在COMMIT时清理这些块。否则,它只会将其忽略(也就是说不清理)。

如果你有如下的处理,就会受到块清除的影响:

l         将大量新数据批量加载到数据仓库中;

l         在刚刚加载的所有数据上运行UPDATE(产生需要清理的块);

l         让人们查询这些数据。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics