5.1 什么是redo日志
REDO日志文件是Oracle数据库实例恢复机制中最为关键的组成部分。REDO日志机制的目的就是确保数据库实例或者服务器发生故障时,不会导致数据库崩溃,不会丢失已经提交的数据。oracle为了实现这个目标,将数据库的变化数据记录在REDO日志文件中,而且通过Redo-Writ-Ahead(RWA)机制确保数据库被变更之前,其变更的REDO日志信息必须先写入日志缓冲区,而事务提交之前也必须首先将日志缓冲区中和这个事务相关的REDO信息写入REDO日志文件。(先写日志到缓冲区,再写数据,即日志优先。)
采用这个机制,oracle就可以确保在数据库宕机后,只要重新启动实例,数据库中没有被及时写入数据文件的DB Cache信息和一些不一致的未提交的事务信息会被正确恢复。(两类问题:(1)已提交事务,日志落盘,但是数据仅仅修改了cache,数据未落盘;(2)未提交事务:日志没落盘)
5.2 REDO的基本原理
5.2.1 介质恢复和实例恢复的基本概念
REDO日志是oracle为确保已提交的事务不会丢失而建立的一种机制。实际上,REDO日志的存在是为两种场景准备的,一种我们称为实例恢复,另一种称为介质恢复。实例恢复的目的是在数据库发生故障时,确保数据块缓冲区中的数据不会丢失,不会造成数据库的不一致。介质恢复的目的是当数据文件发生故障时,能恢复数据。(两种:一种是宕机导致的内存数据丢失,一种是硬盘故障导致的文件丢失)
对于介质恢复和实例恢复来说,第一个步骤都是通过REDO日志的信息进行前滚。在做前滚时,通过REDO日志文件里记录的数据库变化矢量,提交到相关的数据文件中,从而使数据文件的状态向前滚动。大家要注意的是,UNDO表空间的变化也被记录到REDO日志中了,因此UNDO表空间的相关数据文件也会被前滚(对于一个update语句,需要将update之前的值写入到undo表空间中,这个写UNDO表空间的动作也要记录到REDO日志中)。当前滚到最后一个可用的REDO日志或归档日志时,所有的数据恢复层面的工作就全部完成了。这个时候,数据库包含了所有的被记录的变化,这些变化中有些是已经提交的,而有些是尚未提交的。在最新状态的UNDO表空间中,我们也可以看到一些尚未提交的事务。
因此数据库下一步需要做的就是事务层面的处理,回滚那些尚未提交的事务,以确保数据库的一致性。
oracle数据库高速缓存机制是以性能为导向的,高速缓存机制应该最大限度地提高数据库的性能,因此缓存被写入数据文件时总是尽可能推迟。这种机制大大提高了数据库的性能,但是当实例出现故障时,可能存在一些问题:磁盘文件中丢失了某些已提交事务对数据文件的修改信息;未提交事务已经写盘;原子变更一部分写盘,一部分没写。
有两个问题需要解决:1.如何确保已经提交的事务不会丢失。2.如何在数据库性能和实例恢复所需要的的时间上进行平衡,既确保数据库性能不会下降,又保证实例恢复可以快速进行。
解决第一个问题:Log-Force-at-Commit,即在事务提交时,和这个事务相关的REDO日志数据,包括COMMIT记录,都必须从日志缓冲区中写入REDO日志文件,此时事务提交成功的信号才能发送给用户进程。
解决第二个问题:检查点。在oracle数据库中,对缓冲缓存的修改操作是由前台进程完成的,但是前台进程只负责将数据库从数据文件中读到缓冲区中,不负责将缓冲区中修改过的数据写入数据文件。缓冲区写入数据文件的操作是由后台进程DBWR来完成的。DBWR可以根据系统的负载情况以及数据块是否被其他进程使用,来将一部分数据块协会到数据文件中。在这种机制下,某个数据块被写回文件的时间可能具有一定的随机性,有些先修改的数据块可能比较晚才被写入数据文件。而检查点机制就是对杭书记指的一种有效补充。检查点发生的时候,检查点进程会要求DBWR进程将某个SCN以前的所有被修改的块都写回数据文件。这样,一旦这次检查点完成,这个SCN前的所有数据变更都已经存盘,如果之后发生了实例故障,在进行实例恢复时,只需要从这侧检查点已经完成后的变化量开始就行了,检查点之前的变化就不需要再去考虑了。
为了解决数据文件已经写盘,但REDO日志信息还在日志缓冲区中的情况,采用Write-Ahead-Log方式,即写入日志优先。日志写入优先包含两个方面的算法。第一个方面是,在某个缓冲区缓存的修改变化矢量还没有写入REDO日志文件之前,这个修改后的缓冲区的数据不允许被写入到数据文件,这样就确保了在数据文件中不可能包含未在REDO日志文件中记录的变化(日志没写盘时,不许数据写盘)。第二个方面是,在对某个数据的UNDO信息的变化矢量没有被写入REDO日志之前,这个缓冲区的修改不能被写入数据文件(前像没记录在REDO之前,UNDO信息未写盘前,不许数据写盘)。
5.2.2 变化矢量和REDO记录
首先我们要了解的就是变化矢量(Change Vector,CV)。变化矢量是组成REDO信息的基础,一个变化矢量描述了对一个独立数据块进行的一次独立修改操作。这里要注意的是,CV的定义里包含了两层含义,即一个CV只针对一个数据块的变更,一个CV只包含一个变化。每个CV都包含了对文件的修改,因此在每个CV中都有一个OPCODE指出修改的类型。不同OPCODE的CV,其组成也是不同的。
REDO记录是由一组CV组成的,这组CV完成对数据库的一个原子修改操作。比如,一个REDO记录里可能包含3个CV,第一个是对UNDO SEGMENT HEADER的修改,第二个是对UNDO SEGMENT的修改,第三个是对DATA BLOCK的修改。而一个事务可能包含多个REDO记录。
当前台进程要对某个数据块进行修改的时候,(1) 首先要形成相关CV,(2)然后把多个CV组成REDO记录,(3)再把REDO记录写入日志缓冲区后,(4)前台进程可以将CV提交到相关的数据块上。
5.2.3 日志缓冲和LGWR
REDO日志产生的非常频繁,如果每次REDO产生后都必须写入REDO日志文件,那么就会存在两个问题。第一个REDO日志文件写入的频率过高,对导致其I/O性能出现问题,第二个是如果前台进程来完成REDO日志的写入,那么会导致大量并发的前台进程产生对REDO日志文件的争用。为了解决这两个问题,oracle在REDO日志机制中引入了LGWR后台进程和日志缓冲。
日志缓冲用来缓存前台进程产生的REDO日志信息,前台进程可以将产生的REDO日志信息写入日志缓冲,而不需要直接写入REDO日志文件,这样就大大提高了REDO日志产生和保存的时间,从而提高数据库在高并发情况下的性能。
既然前台进程不将REDO日志信息写入日志文件了,那么就必须要有一个后台进程来完成这个工作。这个后台进程就是LGWR,LGWR进程的主要工作就是将日志缓冲中的数据批量写到REDO日志文件中。在oracle数据库中,只要数据库的改变被写入到REDO日志文件中,那么就可以确保相关的事务不会丢失了。
了解日志缓冲和LGWR的算法,有助于我们分析和解决相关的性能问题。用一句话来概括——日志缓冲是一个循环使用的顺序型缓存。这里包含了两层含义。首先,日志缓冲是顺序读写的缓冲;其次,日志缓冲是循环缓冲,当日志缓冲写满后,会回到头部来继续写入REDO日志信息。日志缓冲数据的写入是由前台进程来完成的,并且是并发的,每个前台进程在生成了REDO日志信息后,需要首先在日志缓冲中分配空间,然后将REDO日志信息写入到日志缓冲中去。在日志缓冲中分配空间是一种串行的操作,因此oracle在设计这方面的算法时,把日志缓冲空间分配和复制REDO日志数据到日志缓冲这两种操作分离了。一旦分配了日志缓冲空间,就可以释放相关的闩,这样,其他前台进程就可以继续分配空间了。
前台进程写入REDO信息会使日志缓冲的尾部指针不停地向前推进,而LGWR这个后台进程会不停的从日志缓冲的头部指针开始查找还未写入REDO日志文件的日志缓冲信息,然后将这些信息写入REDO日志文件中,并将缓冲头部指针不停地向后推进。(环队列)
为了让LGWR尽快将日志缓冲中的数据写入REDO日志文件,以便腾出更多的空闲空间,oracle数据库设计了LGWR写的触发条件:
(1)事务提交时
(2)日志缓冲中的数据超过1M时
(3)当日志缓冲中的数据超过了_log_io_size隐含参数指定的大小时。
(4)每隔3秒
下面我们来介绍LOG FILE SYNC等待事件。该事件的作用是等待LGWR将日志缓冲中的数据写入REDO日志文件。一般情况下,某个事物在做提交时,会等待LOG FILE SYCN事件,而没有做提交操作的会话则不需要等待,因为前台进程只需要将REDO日志信息写入日志缓冲就可以了,不需要等待这些数据被写入REDO日志文件。不过前台进程在分配日志缓冲时,如果发现日志缓冲的尾部指针已经追上了头部指针,那么前台进程就要等待LGWR进程将头部的数据写入REDO日志文件,然后释放日志缓冲空间。这是,没有做提交操作的前台进程将头部的数据写入REDO日志文件,然后释放日志缓冲空间。这时没有做提交操作的前台进程都会等待LOG FILE SYNC时间。这种情况下,加大日志缓冲就可以减少大部分的LOG FILE SYNC等待。
5.2.4 日志切换和REDO日志文件
当前台进程在日志缓冲区中分配空间的时候,实际上已经在REDO日志文件中预先分配了空间。如果REDO日志文件已经写满,无法再分配空间给前台进程,就需要做一次日志切换。这时前台进程会向LGWR发出一个日志切换的请求,然后等待LOG FILE SWITCH COMPLETION事件。
日志切换请求发出后,CKPT进程会进行一次日志切换CHECKPOINT,而LGWR开始进行日志切换工作。首先LGWR进程会通过控制文件中的双向链表,查找一个可用的REDO日志文件,将其作为当前REDO日志。次查找算法要求该日志是非活动的,并且已经完成了归档(如果是归档模式)。oracle会优先使用空闲状态的REDO日志组作为当前REDO日志。
在做日志切换时,首先要将日志缓冲中还没有写入写入REDO日志文件的REDO记录写入当前REDO日志文件,然后将最后一个REDO记录的SCN作为该日志文件的HIGH SCN记录在REDO日志文件头中。这些操作完成后,就可以关闭当前日志了。
完成了上一步骤后,就需要进行第二次控制文件事务,将刚刚关闭的REDO日志表示为active,将新的当前REDOLOG标识为current。如果数据库处于归档模式,还要将旧日志组记录到控制文件归档列表记录中,并且通知归档进程对该日志文件进行归档。
旧日志组被目前仍被标志为active,当DBWR完成了CHECKPOINT所要求的批量写操作后,该日志组的状态会被标识为inactive。
从上述日志切换的步骤可以看出,日志切换包含了很多工作,而且在整个过程中,日志的生成都是被完全禁止的,因此在这期间,对数据库的修改操作会被全部阻塞。因此我们常说:“日志切换是一种较为昂贵的操作。”(这里我原来有个问题,为什么不能再切换日志的同时,处理事务,将日志写到缓存中?后来我想起来,书中书,在将日志写到缓冲区的时候,其实已经确定了其写到日志文件中的位置了,因此必须阻塞)
5.2.5 事务提交和回滚的过程
以前有人在面试DBA是经常会问,1万行记录的提交和1行记录的提交在速度上是否相同。要想回答这个问题,就要了解oracle提交的机制。
第一步是服务器进程将提交记录写入REDO日志缓冲区,第二步是LGWR进程相关的REDO记录写入REDO日志文件,第三步是服务器进程将提交完成的消息通知用户进程,第四步是服务器进程继续进行后续处理。决定提交操作速度的因素实际上是前面三个步骤,其中第一步和第三步基本相同,唯一的区别是第二步LGWR进程将日志缓冲区写入REDO日志文件的等待时间。如果REDO日志缓冲区中数据挤压不多,那么这个时间几乎可以忽略不计,一般在1-4ms。如果REDO日志缓冲区中积压了较多的数据,那么LOG FILE SYNC等待的时间就会略长一些,不过在系统IO性能较为正常的时候,这个等待一般在10s以内。因此我么你可以说,无论事务大小,其提交的速度基本上相差无几。
反过来,对于回滚的操作,就没这么简单了,一个修改了数百万记录的事务,回滚操作可能需要数分钟,甚至几十分钟。