数据库的事务与并发控制
文章目录
1、事务简介
事务(Transaction)是指用户定义的一个数据库操作序列,这些操作要门全做,要么全都不做,是一个不可分割的整体。 我想,99%的在阐述事务的概念时都会用银行转账的例子,那么我也用这个例子来阐述,因为它更容易理解。在银行转账业务中,顾客认为支票帐户向存储用户的资金转账就好似一个事务,这个动作要么全部执行,要么什么都不执行,如果转出用户在转账过程中他的金额成功减少了,但是收款方并没有收到,如果出现问题就会导致A的白花花的银子确实减少了,但是B的帐户确没有多出的银子,这就出现了无缘无故A的前少了的情况。因此在数据库系统中对事务的控制非常重要。
事务分为隐形事务和显示事务,DBMS按照默认规定将一条sql语句认为是一条隐式事务;而显式事务需要通过sql语句显式指定。
一般来讲,事务具有以下4个特征,简称为ACID:
- 原子性(Atomicity): 指事务是数据库的逻辑工作单位,事务中的所有操作,要么全做,要么全部不做。
- 一致性(consistency): 当数据库中只包含成功事务的提交结果时,就说数据库处于一致性状态。什么是不一致的状态呢?也就是说在执行事务时,假如由于断电等原因,事务中的一些操作已经被写入物理数据库,而一些操作没有被写入,那么此时就处于不一致的状态。
- 隔离性(Isolation): 指数据库中一个事务在执行过程中不能受到其他事务的干扰,该事务的内部操作及数据对象的使用对其他事务是隔离的。
- 持续性(Durability): 指事务一旦提交,对数据库所做的更改就是永久的,这是保证数据库数据安全的保证。
2、并发控制
在数据库中,串行执行的方式就意味着,一个事务必须等到另一个事务结束后才能执行,这种执行方式使数据库的系统资源得不到更好地发挥不能发挥数据库共享资源的特点。
当使用并发的方式执行事务时,可能存在不同的事务同时存取同一个数据对象的情况,这样就可能造成,读取或写入不正确的数据,破坏数据库的一致性。因此,必须提供并发的控制机制,来防止这些情况的发生,并发控制是DBMS性能的重要标志。一般来讲并发可能会发生3种情况导致数据库的一致性被破坏,分别为:丢失修改,不可重复读,读 “脏“数据。
-
丢失修改(Lost Update): 指两个事务T1和T2读入同一数据并进行修该,T2提交的结果破坏了T1提交的修改,我们用一个图来说明
T1, T2执行后,我们希望的结果明明是让A=8,可是这个时候A=9,因此可以看到数据的一致性被破坏了。 -
不可重复读(Non-Repetable Read): 事务T1读取数据后,T2对数据进行更新操作,使得T1无法再现之前的结果。具体包括3种情况:
(1)T1读取某个数据后,T2进行修改,当T1再次读数据与前一次是不同的结果;
(2)T1读取数据后,T2删除其中的部分记录,当T1再次读取时,发现某些记录已经消失了,也叫做 ”虚读“。
(3)T1读取数据后,T2对数据中插入了某些记录,当T1再次读取时,发现多了一些记录,这也叫 ”幻读“。
下图阐述了第一种情况: -
读 ”脏”数据(Dirty Read): 指一个事务读取了某个失败事务运行过程中的数据。如下图
-
2.2 可串行化调度(Serializable)
DBMS要控制事务地并发执行,保证数据库的一致性,那么需要怎么样的调度方式才能做到正确呢?当且仅当执行结果与按照某一顺序串行地执行这些事务时的结果相同,那么称这种调度是可串行化的调度。
简单地说就是如果有两个事务T1和T2,当它们并发执行的时候,其结果和从T1到T2串行执行或者从T2到T1串行执行的结果相同则说明该并发的调度是正确的调度。
3、锁协议
对于数据库的并发控制技术有封锁、时间戳、乐观控制法。目前大多DBMS一般都采用了锁的技术。
锁的机制是确保事务并发调度是可串行化的一种常用技术,所谓锁就是让事务对数据对象具有一定的控制,一般分为3步:
(1)申请加锁,事务在对数据对象进行操作前对数据提出加锁的请求。
(2)获得锁,当条件满足时,系统允许事务对该数据对象进行加锁,从而对数据获得控制权。
(3)释放锁,操作完成后,事务放弃对数据对象的控制权。
基本的锁类型有两种:排他锁(Exclusive Lock)和共享锁(Share Locks)。排他锁又叫写锁,简称X锁,如果T获得数据对象的X锁,则只允许T读取和修改此数据对象,其他事务不能对此数据对象加任何类型的锁,直到T释放X锁;共享锁又叫读锁,简称为S锁,如果一个事务T获得数据对象的共享锁,则允许T读取数据,其他事务只能再对这个数据对象加S锁,而不能加X锁,直到T释放该数据对象上的S锁。
通过封锁来达到并发控制的目的就必须要选择合适的锁,以及遵守一定的规则,什么时候申请锁,锁的时间,释放时间等,这些就叫锁的协议。
事务T在修改数据对象之前先对数据对象加X锁,直到事务结束(commit或rollback)才释放。 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。在这种协议之下如果另一个事务不需要修改数据,则不需要加锁,因此这种协议,不能保证可重复读和不读“脏”数据。我们以丢失修改中的例子来描述一下一级封锁协议过程:
在一级封锁协议的基础上增加T对要读取的数据对象加S锁,读完后释放S锁。 二级封锁协议不仅可以防止丢失修改,还可以防止读“脏”数据。在二级封锁协议中,由于事务T读完数据后立即释放S锁,因此它不能保证可重复读数据。以下为二级封锁协议的过程:
一级封锁协议加上事务T对要读取的数据对象加S锁,直到事务结束才释放。 三级封锁协议可满足,防止丢失修改,防止读“脏”数据,保证可重复读。
这三级封锁协议区别在于哪些操作需要申请锁以及何时释放锁。随着锁的协议地身高可以避免跟多不一致的情况出现。但是数据对象被封锁的时间越长,并发执行的效率就越低。这里我们再说说两段封锁协议, 两段封锁协议是指,所有事务必须分为两个阶段对数据进行加锁和解锁,其一:在对任何数据对象进行读、写操作之前,事务首先要获得对该数据对象的封锁;其二:在释放封锁之后,事务不再申请和获得任何锁。简单地说就是,事务在执行过程中不允许加锁和解锁交叉执行,加锁动作都是在所有释放锁的动作前。
4、活锁和死锁
锁的机制可以解决并发操作带来的数据库不一致问题,同样锁也会带来一些问题,这就是活锁和死锁。
当事务T1对数据对象加锁后,T2又申请对该数据对象加锁,这是T3也请求对该数据对象加锁,这时它们都在等待T1释放对该数据对象的锁。等到T1释放了该数据对象的锁后,该锁被T3持有,于是乎,T2又继续等待。这时又有T4,T5…又申请对该数据对象加锁。因此T2可能永远在等待,这就是活锁。避免活锁的简单方法就是使用队列,谁先来谁持有。
当T1事务持有某数据对象d1的锁的时候,T2事务也持有数据对象d2的锁,此时T1又申请持有d2的锁,这时T2又申请持有d1的锁,因此你在等我,我在等你两个事务永远也不能结束,这样就形成了死锁。在数据库中解决死锁有两种思路:预防死锁和死锁发生后的诊断和解除。
我们知道死锁产生的原因就是多个事务已经封锁了某些数据对象,但是又申请了其他已经被封锁的数据对象而形成的循环。预防死锁一般有两种方法,但都是在操作系统中比较广泛采用的方法,但是在数据库中并不常用。但是我还是说一下,通常有两种策略:一次封锁法和顺序封锁法。
所谓一次封锁法就是必须一次性将所要用到的数据对象全部封锁,否则就不能执行。例如T1对数据对象d1和d2全部加锁,另一个T2若想要加锁就必须等到T1执行完毕才能持有锁因此就不会出现死锁。但是这样有缺点,其一,如果一次性封锁需要用到的数据对象就一定会扩大锁的范围和时间,这样就会降低系统的并发效率;其二,由于数据库中的数据不断变化,原来不需要加锁的数据可能在执行过程中就变成需要加锁的对象。因此只能再次扩大范围,这样就进一步降低了并发性。
顺序加锁法就是预先对数据对象规定一个加锁的顺序,所有的事务都按照这个顺序对数据对象加锁。但是这样也有问题,其一,数据库中的数据对象非常多,而且不断变化,维护可变性太强的数据成本高难以实现;其二,事务的加锁请求随着事务的执行而动态决定,很难维护这样的顺序。
死锁的诊断一般使用超时法和等待图法,这里面与操作系统中所采用的策略类似。
所谓超时法就是如果一个事务的等待时间超过了规定的时间,就认为发生了死锁。超时法的优点是简单容易实现。但是缺点就是容易产生误判,如果事务是由于客官的原因而造成的等待时间过长,就会被系统认定为发生了死锁。如果将时间间隔设置太长的话,就会导致无法及时处理死锁。
等待图法就是用事务等待图动态反映所有事务的等待情况,等待事务图是一个有向图,图的结点为一个事务,有向边代表等待关系,如下图:
左边显示的就是一个没有死锁的无环等待图,从图中可以看到T1等待T2,T1等待T3,而T3又等待T2,因此当T2的锁释放的时候,T1和T3就能拥有锁。对于右边不仅有大环还有小环可以看到T1和T2和T4之间形成了环,这样就是死锁了。
数据库管理系统的并发控制系统会周期性地生成事务等待图,检测事务是否有环,如果发生环路就形成了死锁。一旦产生死锁就要解决,一般解决的思路就是选择一个处理代价最小的事务rollback。
5、锁的粒度
加锁对象的大小就称为锁的粒度。加锁的对象可以是逻辑单元也可以是屋里单元,总之在并发系统中,可能会产生资源竞争的都可以加锁。例如关系数据库中的属性,元组,表。或者屋里单元中的page,块等。
锁的粒度和系统的并发程度密切相关。锁的粒度越大,系统开销就越小同时系统的并发程度就越低;相反,锁的粒度越小,系统开销就越大,同时并发程度就越高。因此一个好的系统应该提供多粒度锁 来支持不同事务的选择。选择加锁的粒度时应该同时考虑系统的开销和并发程度。