|
数据库是一个共享资源,可供多个用户使用,允许多个用户同时使用的数据库系统称为多用户数据库系统。在单处理机系统中,事务的并行执行实际上是这些并行事务的并行操作轮流交叉运行;在多处理机系统中,每个处理机可以运行一个事务,多个处理机可以同时运行多个事务,实现多个事务真正的并行运行。本节讨论的是以单处理机系统为基础的,这些理论可以推广到多处理机的情况。
|
|
|
当多个用户并发地存取数据库时就会产生多个事务同时存取同一数据的情况,若并发操作不加控制,就可能会存取和存储不正确的数据,破坏数据库的一致性。并发操作带来的数据不一致性包括三类:丢失修改、不可重复读和读“脏”数据。丢失修改是指两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。不可重复读是指事务T1读取数据后,事务T2执行更新操作,使T1无法再现前一次读取结果,具体来讲还包括三种情况:①事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值;②事务T1按一定条件从数据库中读取了某些数据记录后,事务T2删除了其中部分记录,当T1再次按相同的条件读取数据时发现某些记录已经消失了;③事务T1按一定条件从数据库中读取某些数据记录后,事务T2插入了一些记录,当T1再次按照相同条件读取数据时发现多了一些记录。读“脏”数据是指事务T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤销,这时T1修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,即T2读到了“脏”数据。
|
|
|
|
并发控制的主要技术是封锁,所谓封锁就是事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求对其加锁,加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。
|
|
|
基本的封锁类型有两种:排它锁(简称X锁)和共享锁(简称S锁)。排它锁又称写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁,这就保证了其他事务在T释放A上的锁之前就不能再读取和修改A。共享锁又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能在对A加S锁,而不能加X锁,直到T释放A上的S锁,这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
|
|
|
|
运用X锁和S锁这两种基本封锁时,还需要约定一些规则(例如何时申请X锁或者S锁、持锁时间、何时释放等),这些规则称为封锁协议。下面介绍的封锁协议对封锁方式规定不同的封锁规则,在不同程度上解决了对并发操作的不正确调度所带来的问题。
|
|
|
一级封锁协议是:事务T在修改数据R之前必须先对其加上X锁,直到事务结束(包括正常结束和非正常结束)时才释放。一级封锁协议可防止丢失修改,并保证事务T是可恢复的。在这一级的封锁协议中,如果仅仅是读数据而不对其修改的话,是不需要加锁的,所以他不能保证可重复读和不读“脏”数据。
|
|
|
二级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加上S锁,读完后即可释放S锁。这就防止了丢失修改,还可以进一步防止读“脏”数据,但它不能保证可重复读。
|
|
|
三级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。这就防止了丢失修改和不读“脏数据”,还进一步防止了不可重复读。
|
|
|
两段锁协议是:对任何数据进行读写之前必须对该数据加锁,在释放了一个封锁之后,事务不再申请和获得任何其他封锁。这就缩短了持锁时间,提高了并发性,同时解决了数据的不一致性。
|
|
|
|
举个例子来说明活锁的概念,如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。若T3也请求封锁R,当T1释放了R上的锁之后系统首先批准了T3的请求,而T2仍等待。之后T4又请求封锁R,当T3释放了R上的封锁后系统批准了T4的请求,如此继续下去,T2有可能永远等待,这就形成了活锁。避免活锁的简单方法是采用先来先服务的策略。
|
|
|
举例来说明死锁的概念,如果事务T1封锁了数据R1,T2封锁了数据R2,然后T1又请求封锁R2,因为T2已经封锁了R2,所以T1等待T2释放R2。接着T2又申请封锁R1,而T1已经封锁了R1, T2则只能等待T1释放R1上的锁。这样就出现了这样的情况,即T1在等待T2,而T2又在等待T1,T1和T2两个事务永远不能结束,这就形成了死锁。目前在数据库中解决死锁问题主要有两种方法,一个是采取一定的措施来预防死锁的发生,另一个是允许发生死锁,并采用一定手段定期诊断系统中是否有死锁,如果发现了死锁则立即解除掉。
|
|
|
|
死锁的预防通常有两种方法:一次封锁法和顺序封锁法。
|
|
|
一次封锁法要求每个事务必须一次把所有要使用的数据全部加锁,否则就不能继续执行。这个方法虽然能够有效地防止死锁的发生,但是将全部要用到的数据加锁扩大了封锁的范围,降低了系统的并发度。此外,数据库中的数据不断变化,难以精确地确定每个事务要封锁的数据对象,为此只能扩大封锁范围并将所有可能要封锁的数据对象加锁,这就进一步降低了并发度。
|
|
|
顺序封锁法是预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁。例如在B树结构的索引中,可规定封锁的顺序必须是从根结点开始,然后是下一级的子女结点,逐级封锁。顺序封锁法可以有效地防止死锁,但也同样存在问题。第一,数据库系统中封锁的数据对象极多,并且随着数据的插入、删除等操作不断变化,维护这样的资源的封锁顺序非常困难;第二,事务很事先确定每个事务要封锁的全部对象,因此也就很难按规定的顺序施加封锁。
|
|
|
因此在数据库中广为采用的预防死锁的策略并不很适合数据库的特点,而DBMS在解决死锁问题上普遍采用的是诊断并解除死锁的方法。
|
|
|
|
数据库系统中死锁的诊断与解除的方法与操作系统类似,一般使用超时法或事务等待图法。
|
|
|
超时法是指如果一个事务的等待时间超过了规定的时限,就认为发生了死锁。超时法实现起来很简单,但它的不足之处是:①可能会误判死锁,事务可能是因为其他原因而使等待时间超过时限,系统会误认为发生了死锁;②如果时限设得太长,死锁发生后就不能及时发现。
|
|
|
事务等待图是一个有向图G=(T,U)。T为结点的集合,每个结点表示正在运行的事务,U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1和T2之间划一条从T1指向T2的有向边。事务等待图动态地反映了所有事务的等待情况。并发控制子系统周期地检测事务等待图,若发现图中存在回路,则表示系统中出现了死锁。
|
|
|
DBMS的并发子系统一旦检测到系统中存在着死锁,就要设法解除。通常的办法是选择一个代价最小的事务将其撤销(恢复该事务所执行的数据修改操作),释放此事务持有的所有的锁,这样其他的事务就可以运行下去。
|
|
|