简介

保证数据的一致性是数据库的一个最最基本的功能,那数据库在机器down机或者出现其他意外的情况下是如何去保证数据库的数据的一致性的呢?数据库本身主要依靠undologredolog两种日志文件去保持数据的一致性,本文将围绕undolog进行介绍。如何利用undolog去实现数据库的一致性。

数据库架构简介

要介绍数据库一致性的实现机制,自然少不了要介绍下数据库的整体架构,这里画一个简图来介绍下数据库的架构。(因为数据库的架构不是本文重点,所以这里就画个简图,想要详细了解数据库架构的,可以再查阅其他文章。)

浅析数据库事务中的故障恢复_第1张图片

将数据库简化,主要分这么几块:

§ 查询处理器,主要负责针对于查询sql的解析、执行计划的选择等等。

§ 事务管理器,事务是数据库操作的最小单位,事务管理器主要针对于分配事务id等等管理工作

§ 日志管理器

§ 恢复管理器

§ 缓冲管理器,这个大家也清楚,数据库中的写操作都是在缓冲器中完成,然后再flush到硬盘上。

§ 硬盘数据,日志无论是数据库的数据、还是日志文件,都是最终要写到硬盘上做持久化存储的。

下面就针对于上面说的这几块组成部分,来描述下数据库是如何做灾难恢复的。这一篇主要还是讲undolog,下一篇再讲redolog

undo日志简介

undo日志,顾名思义,就是撤销日志,也就是说,这个日志里面记录了相关的撤销操作。通过刚才的数据库架构简图,我们也可以看到,针对于写数据等处理主要还是在内存中进行,既然在内存中进行,那就会出现由于机器宕机导致的丢数据问题。那数据库是如何通过undo日志去保证数据的一致性的呢?

要描述这个问题,我们需要先定义几种操作。假设我们现在要做这样一件事情,需要把某条数据X从数据库中读出来,然后再去改变他的值,改为Y,然后再写回去。好,这样一个操作,数据库可能要走这样几个流程,首先会看缓冲区里有没有,假设有则直接将数据返回,我们称这样一个过程为Read(X),假设缓冲区里没有,那就需要先从硬盘读到缓冲区,然后再返回给用户,那我们定义从硬盘读到缓冲区的过程为Input(X),也就是说,如果缓冲区里没有,那数据库要先经过一个Read(X),然后再经过一个Input(X),然后再经过一个Read(X)。那修改时也一样,数据库要修改缓冲区的内容,那这个操作我们成为Write(Y).还要经过一个从内存刷到磁盘的过程,这个过程我们成为output(Y)。好,有了这几个定义,我们就挨个从这几个过程去分析,如果数据库再这中间某个过程挂了,如何去保证数据的一致性。

在介绍前,先简要说下undolog日志的格式,undolog日志的格式是这样的 ,T就代表事务IDA就代表某一行的某一列,X就代表原先的值。也就是说这条日志就代表T这个事务时,A原先的值为X,对,undolog仅仅记录原先的值,他并不关心你把它改成了多少,他关心的就是原先是多少,因为将来只是做撤销工作。除了这个之外,undolog中还记录start ,意为开启一个事务,commit,意为提交一个事务。大体我们就可以先抽象成这几个。

那比如我们要做上面那个问题,即读取一个值再修改(假设不在缓冲区中),那就要经过以下几步:

见表格:

编号

操作

undolog

1

start

2

Read(X)

3

Input(X)

4

Read(X)

5

Write(Y)

6

7

flush undolog


8

Output(Y)

9

10

commit

11

end

10

flush undolog



我们看上面这个表格,下面我就解释下这个表格。首先,我们需要明确的一点是,无论是操作数据库中的数据,还是日志,都是先在内存中操作,然后flush到硬盘上。这一点是毫无疑问的。

4步应该都容易理解,一开始在undolog中需要记录一个start的标志位,然后234步都是读取数据库的内容,第5步往内存中写,将X的值改为Y,然后第6undolog中会记录下事务TA的原先值是X,那到了第7步呢?是到底是应该先将undolog进行flush,还是应该output后再flush呢?

我们做个假设,假设output后再进行logflush,如果恰好就在output后,数据库down机了,那这样的结果显而易见,日志文件里并没有记载undolog(因为没有flush到硬盘),无法重做所以就会导致数据不一致。所以要在output后进行undologflush是不可取的。

那我们看下上面这个顺序的意义,假设在第6-7步之间宕机了,也就就是在还未flush undolog时已宕机,那不会影响数据的一致性,因为本来数据就没有写到硬盘。如果是在第7-8步间宕机,虽然数据库的数据没有写到硬盘,但log已经flush了,那这时会通过flush后的log重做一遍,因为系统不知道这个log做过了没有,不过即使是重做一遍也不影响最终的数据一致性,也只是将原先的数据又重新写了一遍而已,就是从X写为X,这个不影响数据库一致性,undolog是幂等的,也就是做几次的结果都是一样。所以上面这个顺序才是合理的。

通过undolog进行数据恢复

既然有了undolog,那我们看下数据库是如何通过undolog进行数据恢复的。这时上面架构简图里的恢复管理器就起了作用了,恢复管理器会扫描undolog,找出没有end掉的start,因为从上面的顺序中我们可以看出,“end”记录flushlog中是在事务提交后才flush的,所以,只要是有了end记录了,就说明了这个事务本身已经结束了,数据一致性已经可以保证了。所以恢复管理器这时是扫描没有end配位的start,然后从start开始往后,根据undolog中记录的先前的值进行重做。不过根据我们刚才这个模型会发现,在恢复管理器进行重做时,是不能有其他的写入的,也就是现在的写入应该是夯住状态的。而且还有个问题,恢复管理器需要从头开始对于undolog进行扫描,其实这是不必须的,完全可以有个checkpoint(代表在这之前的数据已经可以保证数据一致性了)的,那恢复管理器只需要找到最后一个checkpoint,然后从checkpoint往后做就可以了。

针对于以上这两个问题,我们下面进行讨论。

 

在上面介绍的模型中,恢复管理器必须要通过全篇扫描整个undolog进行日志恢复,这样做显然是没有太大必要的,因为系统中断肯定是在最后几个事务受到影响,前面的事务应该已经完成commit或者rollback了,不会出现abort的情况,那我们如何知道哪些事务受到了影响呢,如果我们知道了哪一些事务受到了影响,那我们就可以不用全篇进行扫描,而仅仅扫描很小的一部分就可以了。下面就介绍下,数据库如何知道哪些事务受到了影响,数据库为了得到这个目的,引入了检查点(checkpoint)这个概念。

checkpoint 检查点

checkpoint,即检查点。在undolog中写入检查点,表示在checkpoint前的事务都已经完成commit或者rollback了,也就是检查点前面的事务已经不存在数据一致性的问题了。那这个checkpoint如何去实现呢。其实实现的机制很简单,就是周期性的往undolog里面写入。当然这个写入肯定不是随随便便的往里写,在往里写的时候,肯定要检查前面的事务是否完成。

这个时候就会带来一个问题,因为数据库是一直在运行的,也就是事务是在不断启动的,同时可能有n个事务已经处于开始状态。而在检查点往里写的时候,可能又有新的事务启动了,如果让检查点一直等到没有新的事务启动而且前面所有的事务又都提交过了估计很难,那基本检查点就不用往里写了。所以,在这种情况下,只能是在检查点往里写的时候,停止接受新事务,等待已启动的事务提交完毕,然后检查点写入完毕。然后继续接受新事务。类似于这样:例如,现在有T1 T2两个事务,则undolog中写入:

undolog

start T1

start T2

这时到了检查点的周期,要往里写入检查点了,就得等到T1,T2全部提交完毕,然后写入检查点chkpoint。也就是如果现在有一个T3要开启,是无法开启的。系统处于夯住状态。写入完后,开启T3,日志记录如下:

undolog

start T1

start T2

end T1

end T2

chkpoint

start T3

这时候,如果系统挂掉了,故障恢复管理器会从undolog的尾部向前进行扫描,扫描到checkpoint后,就不会往前扫描了,因为前面的事务都已经提交过了,不存在数据一致性问题。所以只需要从checkpoint开始重做即可。

这样固然是好,省掉了需要undolog从头开始扫描的麻烦,但是这样做的缺点也很明显,那就是在写入checkpoint的过程中,系统是出于夯住状态的,所有的写入都要暂停。那能否有一种更好的方法既可以写入checkpoint又不需要系统暂停呢,必须的,当然有,这就是下面要讲的非静态检查点。

非静态检查点

非静态检查点是相对于静态检查点而来的,上文中所提到的就属于静态检查点,因为在检查点写入的同时,系统是不能写入的。而非静态检查点的引入,就是要解决这个问题。

非静态检查点的策略是在写入chkpoint的同时,会记录下当前活跃的事务。比如,当前状态下,T1T2都是活跃状态,那么undolog中会被写入start checkpoint(T1,T2),这时整体系统仍然是正常写入的,也就是说在这条log写入后,仍然可以继续开启其他事务。当T1,T2完成后,会写入end checkpoint的记录。例如如下记录:

undolog

start T1

start T2

start  checkpoint(T1,T2)

start T3

end T1

end T2

end  chkpoint

start checkpoint(T3)

end T3

end  chkpoint

上面这个日志记录就是,在T1,T2开始后,undolog中写入了start checkpoint(T1,T2)的检查点,而这时仍然是可以接受其他事务的开始的,这时有了T3事务的开启。

通过这种机制,可以有效避免在检查点写入时需要停掉服务的弊端,但现在问题又来了,这样写检查点固然是好,但恢复管理器如何通过这样的undolog去进行数据恢复操作呢?因为,如果检查点是静止的,那找到checkpoint后,就不必再往前找了,而现在不一样了,因为找到endcheckpoint后,前面仍可能有未完成的事务,那这时数据恢复是如何恢复的呢?

在这种情况下,数据库宕机后,恢复管理器仍然会从尾往前进行扫描undolog,如果遇到了“end chkpoint”,这时并不代表checkpoint前所有的事务都已经提交了,但我们可以知道,所有未提交的事务都是在上一个startcheckpoint之后,所以会继续往前找,一直找到startcheckpoint,找到startcheckpoint后,比如是startcheckpoint(T1,T2),因为先前已经找到了endchkpoint,所以T1,T2这两个事务已经可以保证数据一致性了,需要重做的就是在startchecpoint(T1,T2)end chkpoint间的这一些非T1T2事务,这些是需要重做的,所以要把这些进行重做。

还有另外一种情况,就是恢复管理器在扫描时,先遇到了startcheckpoint(T1,T2)的日志,在这种情况下,我们首先知道了T1,T2或许是未完成的事务,那这时需要在start checkpoint之后找到是否有某个事务的end语句,如果有,说明这个事务是完成了,如果没有,就说明没有完成,那就要从checkpoint再往后寻找,找到这个事务的start,然后从start之后往后重做。说得比较罗嗦,我们上个例子来说明下这种情况。

例如,数据库宕机后,开始扫描undolog,得到以下片段:

undolog

start T1

start T2

start  checkpoint(T1,T2)

start T3

end T1


这时,恢复管理器拿到这个片段后进行扫描,在遇到end chkpoint前遇到了start checkpoint(T1,T2),这说明了,T1,T2是可能未完成事务的,而且在这之前还遇到了T3start,没有end T3,也没有任何T3的检查点的开始,这说明了T3一定是未完成事务的,所以T3一定是要重做的。先前为什么说T1,T2是可能未完成事务的呢?因为遇到了start checkpoint(T1,T2),没有遇到end chkpoint,并不代表T1T2就一定是未完成的,可能有一个已经commit过了,因为两个都没有commit,所以才导致了没有end chkpoint,所以这时找start下面的日志,发现了“end T1”,说明了T1的事务是已经完成了的。那只需要找T2的开启然后开始重做就可以了,然后就通过start checkpoint(T1,T2)再往上找,找到了start T2,然后开始重做T2,也就是这个日志里,T2T3是需要重做的,然后重做掉。(注:刚才先说了做T3,然后有说了重做T2,并不代表真正的顺序就是这样,实际上恢复管理器是先分析出需要重做的事务,然后一块做掉的。