9 事务管理
9.1 恢复机制
数据对一个单位是非常重要的。也就是说,数据库的失效常常导致一个单位的瘫痪。为此,对数据库出现的故障有备选的方案去处理是非常重要的。
数据库出现故障时处理的重点就突出两个字:防、治
数据库出现故障的时候,如果是防,我们就尽可能提高系统的可靠性;如果防不住我们就治,在数据库系统发生故障后,把数据库恢复到之前的状态。一般来说,我们做第一手的准备不足以保证数据库数据的安全,比如突然停电,这种东西谁都防不住,所以通常在第一手准备的情况下,我们会去做第二手准备,而第二手准备,就是我们讨论的重点:恢复技术。
数据的恢复机制必须要求以下两点:
一、数据冗余
硬件和软件出现故障,就会导致运行出现差错,也就会导致数据库失效。故障是前因,差错是现象,失效是后果。为了恢复失效后果,我们必须留有后备的副本以备不时之需,这样来看的话,备份就会导致数据冗余是必需的。
这里的冗余和数据库设计的冗余不一样,那里的冗余指的是数据库结构为了追求三范式,而有些地方不合理造成的冗余;而我们上面说的冗余是留有数据做备份。
二、自动检测故障
要能够检测出故障,不能出现故障了还没有停下来,稀里糊涂的继续操作下去。
说完数据恢复必要的元素,我们来讲讲恢复技术有哪些。大体来说一般归为以下三类。
单纯以后备副本为基础的恢复技术(周期性转储)
这个是从文件系统继承过来的恢复技术。这里要继续讲下去又得讲一下磁带了。
磁带就相当于小时候的录音带,你甚至可以理解为他是读卡器,他是一种可以脱机的存储设备,但是容易损坏。
所以使用以后备副本为基础的恢复技术时,我们会把磁盘上的数据库周期性转储到磁带上,当然你也可以刻在光盘上。由于磁带可以脱机存放,可以不受系统故障的影响,所以存在磁带的那部分数据库副本我们叫后备副本
。
那你可以试想,数据库如此庞大,假如真的让你那个类似于U盘一样的东西去拷贝,拷贝一次需要多久?而且在拷贝过程中数据库不能改变,相当于冻结了数据库,暂停其运作;所以在取后备副本的时候由于时间问题,我们不能频繁的取,一般周末夜间数据库空闲的时刻才会去操作。
但是这样一来你就会发现一个问题:假如我取后备副本的周期过长,那就会导致假如在上一次取后备副本到数据库突然故障这段期间我做了大量的更新的话,那么就会导致这些操作全部白干。
所以,我们想到了提高这个恢复技术的办法,也就是增量存储
。这实际上是周期性转储方法的变种。一般来说,数据库不会改动整个结构,只会改动其中一小部分,我们可以把那一小部分所对应的物理块拿出来高频(也就是周期很短)地转储,其他的物理块可以很久才转储一次,这样的话,一旦发生故障,马上就能恢复最近更新的那一部分的物理块,损失度大幅下降。
海量转储 | 增量转储 |
---|---|
每次转储全部数据库 | 只转储上次转储更新过的数据 |
对于海量存储和增量存储来说各有好坏:
- 从恢复角度看,使用海量转储得到的后备副本进行恢复往往更方便
- 如果数据库很大,事务处理又十分频繁,则增量转储更有效
由于这种方法实现起来很简单,所以在文件系统使用得很多,但是在数据库系统中只用于小型的和不重要的数据库系统。
以后备副本和运行记录(log或者journal)为基础的恢复技术
这种技术是企业里面用的最多的技术。
在SQLserver中,我们能看到建数据库的时候我们还会建一个日志文件log,这里就派生用场了。日志实际上就是一个流水账,我们也叫他是一个运行记录,记录你所有操作的流水;其中运行记录里面记了两样东西,一种是前像
,一种是后像
。
前像(也就是老的数据,老值)(Before image,简称BI)
如果数据库被事务做了一次更新,那么我们就可以利用还没更新前数据库的数据涉及的物理块来恢复更新前的状态,也就是撤销更新,这种操作我们在恢复技术里叫做撤销
(英文简称undo)。
后像(也就是新的数据,新值)(After image,简称AI)
同样的,我们更新后也有更新后数据涉及的物理块,如果更新的数据突然没了,我们可以根据这个物理块把更新的数据恢复到更新后的状态。这就是重做
(英文简称redo)。
上面可能听的有点蒙,这么说吧,我们有两个备份——前像和后像。其中前像就是一面通往过去的魔镜,后像就是一个通往未来的过程,终点是更新结束。如果你更新到一半卡住了,说你更新了吧,你没更新完,说你没更新,你又开始更新了,那你此时就可以根据过去的魔镜回到过去。如果你更新完成了,但是更新了啥你却丢了。那这个时候你就可以利用后像重新走一次更新过程。
就拿修改表数据的相关操作来说,日志记录以下情况:
for update op:BI AI
insert op:—— AI
delete op:BI ——
事务状态:记录每个事务的状态,方便在恢复的时候做不同的处理。
每个事务在提交的时候面临两种结局:
第一种是提交了才结束,这标志着事务成功地执行了,只有在事务提交后,事务对数据的影响才能被其他事务访问。
第二种是由于事务本身或者外部的原因,事务失败,也就是说这时候可能事务只做了一半,要继续做的时候事务出问题终止了,这时候为了消除那做了一半的影响,我们会对其做出卷回
。
如果是数据库失效了,可以取出后备副本,然后根据运行的记录,如果没提交就将前像卷回,这叫向后恢复
。如果事务已经被提交了,但是提交前和提交后中间添加的数据没了,那我们必要时用后像重做,这叫向前恢复
。
基于多副本的恢复技术
如果系统中有多个数据库副本,而且这些副本具有独立的失效模式
,则可利用这些副本相互备份,用于恢复。所谓的独立失效模式是指各个副本不会因为故障而一起失效。为了尽可能的创建这种环境,各副本的支撑环境要尽可能地独立,比如不要共用电源、磁盘、控制器和CPU等等,反正就是尽可能不要共用东西就对了。这种技术在分布式数据库用的比较多。
由于现在硬件价格下降,很多人都能买得起磁盘,所以在一些可靠性要求高的系统中,常常采用镜像磁盘技术
,镜像磁盘技术为了使失效模式独立,两个磁盘系统有各自的控制器和cpu,但彼此可以相互切换。他的原理如下
- DBMS自动把整个数据库或其中的关键数据复制到另一个磁盘上
- DBMS自动保证镜像数据与主数据库的一致性,每当主数据库更新时,DBMS自动把更新后的数据复制过去,如图:
出现介质故障时
- 可由镜像磁盘继续提供使用
- 同时DBMS自动利用镜像磁盘数据进行数据库的恢复
- 不需要关闭系统和重装数据库副本
没有出现故障时,镜像磁盘可用于并发操作,一个用户对数据加排他锁修改数据,其他用户可以读镜像数据库上的数据,而不必等待该用户释放锁。
9.2 事务和日志
我们在这一讲谈到了多次事务,那事务(transaction)到底是啥呀。
9.2.1 事务
一个事务是一个完整的业务逻辑单元,不可再分。
比如:银行账户转账,从A账户向B账户转账10000,需要执行两条Update语句。
update t_act set balance = balance -10000 where actno = 'act_001';
update t_act set balance = balance + 10000 where actno = 'act_002';
以上这个过程是一起的,如果不一起,就会导致转账这件事失败。也就是说,以上两条DML语句必须同时成功,或者同时失败,不允许出现一条成功,一条失败。
要想保证以上的两条DML语句同时成功或者同时失败,那么就需要使用数据库的“事务机制”。
需要注意的是,和事务相关的SQL语句只有DML语句
,因为它们包含的添加、删除、更新语句都是和数据库表当中的“数据”相关的。事务的存在是为了保证数据的完整性,安全性。
有人会发出疑问:假设所有的事务都能使用一条DML语句搞定,还需要事务机制吗?如果真的出现上述情况,那么确实不需要事务。但实际情况不是这样的,通常一个事务需要多条DML语句共同联合完成。
事务可以保证多个操作原子性,要么全成功,要么全失败。对于数据库来说事务保证批量的DML要么全成功,要么全失败。综上所述,事务具有四个特征:ACID。
- A原子性:整个事务中的所有操作,必须作为一个单元全部完成(或者全部取消),即:要么不做,要么全做
- C一致性:在事务开始之前和结束之后,数据库都保持一致状态
- I隔离性:一个事务不会影响其他事务的运行
- D持久性:持久性说的是最终数据必须持久化到硬盘文件中,事务才算成功的结束。也就是说,一个成功执行的事务对数据库的影响是持久的,即使数据库因故障而受到破坏,DBMS也应该能够恢复
我们来看看一个事务的案例:
Begin transaction
read A
A:=A-s
if A<0 then Display "insufficient fund"
Rollback /*undo and terminate*/
else B:=B+s
Display "transfer complete"
Commit /*commit the update and terminate*/
Rollback --abnormal termination
Commit --normal termination
假如现在做一个银行转账系统,A给B转了s元,如果A余额不足,那么就会提示:insufficient fund(余额不足),然后把A余额-s元的这个操作回滚。
如果余额足够,那么A-s,B+s,然后提示转账成功,然后事务结束,利用Commit提交事务。
9.2.2 运行记录的结构
9.2.2.1 活动事务表
记录运行记录的有多个结构,与操作系统类似,操作系统中有记录进程的进程块PCB,对于事务来说则有标识符(Transaction Identifier,TID)
。对于所有正在执行,尚未提交的事务,其身上的标识符都会在活动事务表(Active Transacion list,ALT)
中占有一条元组。活动事务表会给这些事务贴上正在执行、尚未提交的标识符。
9.2.2.2 提交事务表
提交事务表(Committed Transaction list,CTL)
记录所有已经提交的事务的标识符。
当你把一个事务要提交了,那活动事务表会把TID交给提交事务表,然后在活动事务表中把该TID删除。但是,这两者顺序是不可颠倒的,如果颠倒,万一停电,就会导致TID刚在活动事务表中删除,然后在添加进提交事务表的那一刻由于故障没有任何记录。
9.2.2.3 日志
对于数据库来说,日志(log)
一般记录了操作的流水记录。其一般存储在非挥发存储器(非易失存储器)
中。
存储器一般分为主存和辅存。主存通常指随机访问存储器(RAM),一般分为两种:一种是易失性随机访问存储器,如RAM(磁芯存储器)、CMOS;另外一种是非易失性随机访问存储器,它们和RAM不同,遇到突然的外界因素不会丢失其内容。例如:ROM(只读存储器)、EEPROM(电可擦除可编程ROM)、闪存。
日志主要包含TID,前像文件和后像文件。具体结构如下:
其中前像文件中的BID记录的是物理块的地址,空白部分记录的是正在更新事务的老值,这一个矩形就相当于一个物理块,前像文件相当于一个堆文件,有很多物理块堆叠而成,如果一个关系需要卷回,就可以从前像文件中找出该事务的所有前像块,然后把它们全部写入由于故障被破坏的关系的对应块中从而消除该事务对数据库所产生的影响。同理,后像文件也和前像文件的结构一样。
在每个日志中都应该记录前像文件和后像文件,以方便供以恢复使用。
9.2.3 提交规则和先记后写规则
9.2.3.1 提交规则
后像(新值)必须在事务提交前写入非挥发存储器中,不能放在内存中,因为内存会消失。
也就是说,后像可以放数据库也可以放日志中(前面说过日志也是一个非挥发存储器);万一这时候故障,即使没有放数据库,我们也可以利用日志中的后像文件中的后像重做;如果这时候有其他事务要访问这些数据,由于更新的内容已经保存在后像文件里面了,也可能在缓冲区中(因为后像文件是由多个物理块组成的),所以其他事务仍然可以去缓冲区或者后像文件找哪些更新后的内容。
9.2.3.2 先记后写规则
如果后像
在事务提交前写入了数据库,那必须把前像
首先记入运行记录。
这句话可能有点抽象,我们举个例子。假如我现在在做笔记,如果我没有保存以前写的旧笔记,直接把旧笔记撕了,然后直接写新笔记,这时候一旦突然断电,那就有可能新笔记和老笔记都没了。先记后写规则就是为了防备这一点。
9.3 更新策略以及故障后的恢复
更新操作无非就是undo或redo。需要注意的是,undo操作和redo操作具有幂等性。
举个例子:假如在编程中,我们设x = 10,后面又写了一条x = 20,那么就能把x = 10覆盖掉,其中后像20,前像10,但是后像20覆盖前像10的时候,不管覆盖多少次都是20,这就是幂等性,即做多次操作和一次操作的效果是等价的。
更新操作并不是上来就无脑搞,是有一定策略的。目前理论上的更新策略有以下几种。
后像在事务提交前已经完全写入数据库(没人采用)
策略具体步骤如下:
- TID→ATL
- BI→log
- AI→DB,log
- TID→CTL
- 从ATL删除TID
在每个数据库系统里总会有这么个模块——重启动模块,不同数据库系统叫法可能不同;他的作用是在重启动数据库系统后,检查上一次退出是否正常退出。
在用重启动模块检查数据库系统时,会出现下列三种情况:
ATL | CTL | 事务所处状态 | 恢复措施 |
---|---|---|---|
有 | - | 1已经完成,但4没有完成 | ①若BI已经写入log则undo,否则无需undo ②从ATL删除TID |
有 | 有 | 4执行完成 | 从ATL删除TID |
- | 有 | 5执行完成 | 无需处理 |
- 如果ATL里有而CTL没有,说明在提交阶段出问题了,那么系统会自动去undo收回那些事务。
- 如果ATL有,CTL也有,说明事务已经提交,但是ATL删除TID的时候出问题了,那么系统就会去删除TID。
- 如果ATL没有,CTL有,说明过程执行没有出现差错,事务提交完毕。
后像在事务提交后才写入数据库(目前主流)
策略具体步骤如下:
- TID→ATL
- AI→log
- TID→CTL
- AI→DB
- 从ATL删除TID
TID提交ATL说明执行为提交,这时候由于后像没有在事务提交前写入数据库,所以不必记录前像,这时把后像记到日志,然后TID提交给CTL,这时候确定后像提交了,然后再把AI给DB,然后在ATL删除TID。
在用重启动模块检查数据库系统时,会出现下列三种情况:
ATL | CTL | 事务所处状态 | 恢复措施 |
---|---|---|---|
有 | - | 1已经完成,但4没有完成 | ①从ATL删除TID |
有 | 有 | 3已经完成,但5没有完成 | ①redo ②从ATL删除TID |
- | 有 | 5执行完成 | 无需处理 |
- 如果检查时发现ATL有TID而CTL没有,说明提交CTL过程出错,这时候由于还未在其他地方做出任何操作,无须undo,只需在ATL删除TID即可。
- 如果检查ATL和CTL都有TID,那么说明ATL删除TID出错,甚至AI搬到DB的阶段搬到一般故障了也有可能,所以这时候需要重新开始搬,即redo,然后在ATL里删除TID。由于幂等性,即使你前面搬到一半,现在redo重新搬也不会搬多。
- 如果ATL没有而CTL有说明过程结束了,无须任何操作。
后像在事务提交前后写入数据库(现在过时了)
策略思想:在第二个策略的基础上,人们想了一个办法,同样的不将后像在事务提交前急忙写入数据库,而是创建一个后台进程,当数据库空闲的时候后台进程将其唤醒,开始把后像搬到数据库,一旦数据库忙起来了,就暂停搬运。
- TID→ATL
- AI,BI→log
- AI→DB(部分写入)
- TID→CTL
- AI→DB(继续完成)
- 从ATL删除TID
在用重启动模块检查数据库系统时,会出现下列三种情况:
ATL | CTL | 事务所处状态 | 恢复措施 |
---|---|---|---|
有 | - | 1已经完成,但4没有完成 | ①若BI已经写入log则undo,否则无需undo ②从ATL删除TID |
有 | 有 | 4已经完成,但6没有完成 | ①redo ②从ATL删除TID |
- | 有 | 6执行完成 | 无需处理 |
- 如果ATL有TID而CTL没有,说明BI可能有一部分在搬到数据库了,这时候需要做undo。
- 如果ATL有,CTL也有,说明AI可能搬到一半,ATL还没删除TID,所以为了继续搬完,我们要根据前像做redo,搬完后在ATL删除TID。
- 如果ATL和CTL都有,说明过程全部执行完毕,无须操作。