封锁机制

封锁机制

10.2.1 封锁及锁的类型

封锁机制是并发控制的主要手段。封锁是使事务对它要操作的数据有一定的控制能力。封锁具有3个环节:第一个环节是申请加锁,即事务在操作前要对它欲使用的数据提出加锁请求;第二个环节是获得锁,即当条件成熟时,系统允许事务对数据加锁,从而事务获得数据的控制权;第三个环节是释放锁,即完成操作后事务放弃数据的控制权。为了达到封锁的目的,在使用时事务应选择合适的锁,并要遵从一定的封锁协议。

基本的封锁类型有两种:排它锁(Exclusive Locks,简称X锁)和共享锁(Share Locks,简称S锁)。

(1)排它锁

排它锁也称为独占锁或写锁。一旦事务T对数据对象A加上排它锁(X锁),则只允许T读取和修改A,其他任何事务既不能读取和修改A,也不能再对A加任何类型的锁,直到T释放A上的锁为止。

(2)共享锁

共享锁又称读锁。如果事务T对数据对象A加上共享锁(S锁),其他事务对A只能再加S锁,不能加X锁,直到事务T释放A上的S锁为止。

10.2.2 封锁协议

简单地对数据加X锁和S锁并不能保证数据库的一致性。在对数据对象加锁时,还需要约定一些规则。例如,何时申请X锁或S锁、持锁时间、何时释放等。这些规则称为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。封锁协议分三级,各级封锁协议对并发操作带来的丢失修改、不可重复读取和读“脏”数据等不一致问题,可以在不同程度上予以解决。

(1)一级封锁协议

一级封锁协议是:事务T在修改数据之前必须先对其加X锁,直到事务结束才释放。

根据该协议要求,将表10.1种的任务T1、T2作为事务,用A表示库存,重新执行各操作的过程见表过程见表10.4。

表10.4 遵循一级封锁协议的事务执行过程

顺序

T1

T2

库存A的值

1

Xlock A

获得

50

2

读A=50

Xlock A

等待

50

3

A←A+100

写回A=150

Commit

Unlock A

等待

等待

等待

150

4

获得Xlock A

读 A=150

A←A-40

回写 A=110

Commit

Ulock A 

110

可见,一级封锁协议可有效地防止“丢失更新”,并能够保证事务T的可恢复性。但是,由于一级封锁没有要求对读数据进行加锁,所以不能保证可重复读和不读“脏”数据。表10.5所示的操作过程遵从一级封锁协议,但仍然发生了读“脏”数据错误。读者可以用类似的操作实例,便会发现一级封锁协议也不能避免不可重复读的错误。

表10.5 遵从一级封锁协议发生的读“脏”数据过程

顺序

T1

T2

库存A的值

1

Xlock A

获得

读A=50

A←A+100

写回A=150

Ulock A

50

150

2

读 A=150

150

3

ROLLBACK

50

(2)二级封锁协议

二级封锁协议是:事务T对要修改数据必须先加X锁,直到事务结束才释放X锁;对要读取的数据必须先加S锁,读完后即可释放S锁。

二级封锁协议不但能够防止丢失修改,还可进一步防止读“脏”数据。但是由于二级封锁协议对数据读完后即可释放S锁,所以不能避免“不可重复读”错误。例如,表10.6所示的并发操作执行过程,遵从二级封锁协议,但发生了“不可重复读”错误。

表10.6 遵从二级封锁协议发生的“不可重复读”的过程

顺序

T1

T2

A的值

B的值

1

Slock A,B

获得

读A=50

读B=100

Ulock A,B

Xlock B

等待

等待

等待

获得

50

150

100

2

求和=150

Slock A 得到

Slock B 等待

等待

获得

读 B=100

B←B×4

回写B=400

Commit

Ulock B

50

400

3

读A=50

读B=400

和=450

(验算错误)

50

400

(3)三级封锁协议

三级封锁协议是事务T在读取数据之前必须先对其加S锁,在要修改数据之前必须先对其加X锁,直到事务结束后才释放所有锁。

由于三级封锁协议强调即使事务读完数据A之后也不释放S锁,从而使得别的事务无法更改数据A。三级封锁协议不但防止了丢失修改和不读“脏”数据,而且防止了不可重复读。

10.2.3 封锁出现的问题及解决方法

事务使用封锁机制后,会产生活锁、死锁等问题,DBMS必须妥善地解决这些问题,才能保障系统的正常运行。

1.活锁

如果事务T1封锁了数据R,T2事务又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁之后系统首先批准了T3的要求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后系统又批准了T4的请求,……,T2有可能永远等待。这种在多个事务请求对同一数据封锁时,使某一用户总是处于等待的状况称为活锁。

解决活锁问题的方法是采用先来先服务。即对要求封锁数据的事务排队,使前面的事务先获得数据的封锁权。

2.死锁

如果事务T1和T2都需要数据Rl和R2,操作时Tl封锁了数据R1,T2封锁了数据R2;然后T1又请求封锁R2,T2又请求封锁Rl;因T2已封锁了R2,故T1等待T2释放R2上的锁。同理,因T1已封锁了R1,故T2等待T1释放R1上的锁。由于Tl和T2都没有获得全部需要的数据,所以它们不会结束,只能继续等待。这种多事务交错等待的僵持局面称为死锁。

数据库中解决死锁问题主要有两类方法:一类方法是采用一定措施来预防死锁的发生;另一类方法是允许发生死锁,然后采用一定手段定期诊断系统中有无死锁,若有则解除之。

一般来讲,死锁是不可避免的。DBMS的并发控制子系统一旦检测到系统中存在死锁,就要设法解除。通常采用的方法是选择一个处理死锁代价最小的事务,将其撤消,释放此事务持有的所有的锁,使其他事务得以继续运行下去。当然,对撤消的事务所执行的数据修改操作必须加以恢复。

10.2.3 封锁的粒度

封锁粒度(Granularity)是指封锁对象的大小。封锁对象可以是逻辑单元,也可以是物理单元。以关系数据库为例,封锁对象可以是属性值、属性值的集合、元组、关系、直至整个数据库;也可以是一些物理单元,例如页(数据页或索引项)、块等。封锁粒度与系统的并发度和并发控制的开销密切相关。封锁的粒度越小,并发度越高,系统开销也越大;封锁的粒度越大,并发度越低,系统开销也越小。

一个系统应同时支持多种封锁粒度供不同的事务选择,这种封锁方法称为多粒度封锁 (Multiple Granularity Locking)。选择封锁粒度时应该综合考虑封锁开销和并发度两个因素,选择适当的封锁粒度以求得最优的效果。通常,需要处理大量元组的事务可以以关系为封锁粒度;需要处理多个关系的大量元组的事务可以以数据库为封锁粒度;而对于一个处理少量元组的用户事务,以元组为封锁粒度比较合适。

你可能感兴趣的:(数据库)