多事务执行方式
多个事务如何一起执行呢?
1. 事务串行执行:每个时刻只有一个事务运行,其他事务必须等到这个事务结束后方能运行。(事务一个接一个的运行)
2. 交叉并发方式:在单处理机系统中,并行事务并行操作轮流交叉运行。 这种并行执行方式称为交叉并发方式。
3. 同时并方式:在多处理机系统中,每个处理机可以运行一个事务,多个处理机可以同时运行多个事务,实现多个事务真正的并行运行,这种并行执行方式称为同时并发方式。
以下讨论以单核处机的并发控制为主:
在进行并发操作会带来哪些问题呢?
并发操作会带来的数据不一致性:
1. 丢失数据(W-W)
两个事务T1、T2同时读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失
T2 | |
---|---|
① R(A)=16 |
|
② | R(A)=16 |
③ A=A-1 |
|
W(A)=15 |
|
④ | A=A-1 |
| W(A=15) |
2. 不可重复读(R-W)
事务T1读取某一个数据后,事务T2执行更新操作,使T1无法再现前一次读取结果,包括三种情况:
⑴. T2执行修改操作,T1再次读数据时,得到与前一次不同的值
⑵. T2执行删除操作,T1再次读数据时,发现某些记录神秘的消失了
⑶. T2执行插入操作,T1再次读数据时,发现多了一些记录
(2)(3)发生的不可重复读有时也称为幻影现象。
T1 | T2 |
① R(A)=50 |
|
R(B)=100 |
|
求和=150 |
|
② | R(B)=100 |
| B=B*2 |
| W(B)=200 |
③ R(A)=50 |
|
R(B)=200 |
|
求和=250 |
|
3. 读“脏”数据(W-R)
事务T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤销,这时被T1修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为“脏”数据,即不正确的数据。
T1 | T2 |
① R(C)=100 |
|
C=C*2 |
|
W(C)=200 |
|
③ | R(C)=200 |
③ ROLLBACK |
|
C恢复100 |
|
如何避免发生这种数据不一致的现象?——DBMS必须提供并发控制机制
并发控制机制的任务:对并发操作进行正确调度、保证事务的隔离性、保证数据库的一致性。
并发控制的主要技术有:封锁、时间戳、乐观控制法、多版本并发控制。
封锁是实现并发控制的一个有效措施,那么什么是封锁呢?
封锁是事务T在对某个数据对象(例如表、记录等操作时)。先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。
封锁有哪些类型呢?
- 排他锁:简称X锁(又称写锁),若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何锁。直到T释放A上的锁。
- 共享锁:简称S锁(又称读锁),若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁为止。
T2 T1 | X
| S
| -
|
X | N | N | Y |
S | N | Y | Y |
- | Y | Y | Y |
有了封锁的类型,如何加锁才能使并发操作不会出现数据不一致现象呢?
封锁协议:约定了对数据对象何时申请X锁或S锁,持续时间、何时释放等一系列规则。
1.一级封锁协议:事务T在修改数据R之前必须先对其加X锁,直到
事务结束才释放。(事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK))。
T1 | T2 |
① Xlock A |
|
② R(A)=16 |
|
③ | Xlock A |
④ A=A-1 | 等待 |
W(A)=15 | 等待 |
Commit Unlock | 等待 |
⑤ | 获得Xlock A |
| R(A)=15 |
| A=A-1 |
| W(A)=14 |
| Commit Unlock A |
2. 二级封锁协议:在一级封锁协议的基础上增加事务T在读取数据数据R之前必须先对其加S锁,
读完后即可释放S锁
T1 | T2 |
① Xlock C |
|
R(C)=100 |
|
C=C*2 |
|
W(C)=200 |
|
② | Slock C |
| 等待 |
③ ROLLBACK | 等待 |
C恢复100 | 等待 |
Unlock C | 等待 |
④ | 获得Slock C |
| R(C)=100 |
| Commit |
| Unlock C |
3. 三级封锁协议:在一级封锁协议的基础上增加事务T在读取数据R之前必须先对其加S锁,直到
事务结束才释放。
T1 | T2 |
① Slock A |
|
Slock B |
|
R(A)=50 |
|
R(B)=100 |
|
A+B=150 |
|
② | Xlock B |
| 等待 |
③ R(A)=50 | 等待 |
R(B)=100 | 等待 |
A+B=150 | 等待 |
Commit | 等待 |
Unlock A | 等待 |
Unlock B | 等待 |
④ | 获得Xlock B |
| R(B)=100 |
| B=B*2 |
| W(B)=200 |
| Commit |
| Unlock B |
封锁协议级别越高,一致性程度越高
| X锁 | S锁 | 一致性保证 | ||||
| 操作结束释放 | 事务结束释放 | 操作结束释放 | 事务结束释放 | 不丢失数据 | 不读脏数据 | 可重复读 |
一级封锁协议 |
| √ |
|
| √ |
|
|
二级封锁协议 |
| √ | √ |
| √ | √ |
|
三级封锁协议 |
| √ |
| √ | √ | √ | √ |
封锁会带来哪些问题呢?
1. 活锁:如果事务T1封锁了数据R,事务T2又请求封锁数据R,于是T2等待;T3也请求封锁数据R,当T1释放了R上的锁之后,系统首先批准了T3的请求,T2任然等待;然后T4又请求封锁R,当T3释放R上的锁之后,系统又批准了T4的请求……T2有可能永远等待,这就是活锁的情况。
避免活锁的简单方法是:采用先来先服务策略
T1 | T2 | T3 | T4 |
Lock R |
|
|
|
…… | Lock R |
|
|
…… | 等待 | Lock R |
|
…… | 等待 | 等待 |
|
Unlock R | 等待 | 等待 | Lock R |
| 等待 | 获得Lock R | 等待 |
| 等待 | …… | 等待 |
| 等待 | …… | 等待 |
| 等待 | Unlock R | 等待 |
| 等待 |
| 获得Lock R |
| 等待 |
| …… |
| 等待 |
| …… |
2. 死锁:如果事务T1封锁了数据R1,T2封锁了数据R2,然后T1又请求封锁R2,因T2封锁了R2,于是T1等待T2释放R2上的锁;接着T2又请求封锁R1,因T1封锁了R1,于是T2等待T1释放R1上的锁。这样就出现了T1在等待T2,而T2又在等待T1,的局面,T1、T2两个事务永远不能结束,形成死锁
T1 | T2 |
Lock R1 |
|
…… | Lock R2 |
Lock R2 | …… |
等待 | …… |
等待 | Lock R1 |
等待 | 等待 |
等待 | 等待 |
等待 | 等待 |
解决死锁的方法:有两种思路
1. 预防死锁的发生
① 一次封锁法:一次性将所有要使用的数据全部加锁,否则就不能继续执行
存在的问题:扩大了封锁范围,降低了系统的并发度;
② 顺序封锁法:预先对数据对象规定一个封锁顺序,所有事务都按照这个顺序实施封锁。
存在的问题:
1.数据库在动态地不断变化,要维护这样的资源的封锁顺序非常困难,成本很高。
2.事务的封锁请求可以随着事务的执行而动态地决定,很难实现确定每一个事务要封锁哪些对象,因此很难按规定的顺序去施加封锁。
2. 死锁的诊断与解除(普遍采用的方法)
① 超时法:如果一个事务的等待时间超过了规定的时限,就认为发生了死锁
存在的问题:
1.时间设置太短,有可能误判死锁
2.时间设置太长,死锁发生后不能及时发现
② 等待图法:事务等待图是一个有向图G=(T,U),T为结点的集合,每个结点表示正在运行的事务;U为边的集合,表示事务等待情况,若事务T1等待T2,则在T1、T2之间画一条有向边,从T1指向T2。
事务等待图动态地反映了所有事务的等待情况。并发控制子系统周期性地(如每隔数秒)生成事务等待图,并进行检测。如果发现图中存在回路,则表示系统中出现了死锁。并发控制子系统一旦检测到系统中存在死锁,就要设法解除。通常采用的方法是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,使其他事务得以运行下去。
并发调度的可串行性
数据库管理系统对并发事务的不同调度可能会产生不同的结果,什么样的调度是正确的呢?
可串行化的调度是正确的
可串行性是并发事务正确调度的准则,按这个准则规定,一个给定的并发调度,当且仅当它是可串行化的,才认为是正确调度。
什么是可串行化调度?
多个事务的并发执行是正确的,当且仅当其结果与按某一次串行执行这些事务的结果相同,称这种调度策略为可串行化调度。
如何判断一个调度是否为可串行化的调度?
冲突操作:不同的事务对同一数据对象的读写操作和写写操作
Ri(x)与Wi(x) //事务Ti读x,Tj写x,其中i≠j
Wi(x)与Wi(x) //事务Ti写x,Tj写x,其中i≠j
原则:不同事务的冲突操作和同一事物的两个操作是不能交换的。
一个调度Sc在保证冲突操作的次序不变的情况下,通过交换两个事务不冲突操作的次序得到另一个调度Sc’,如果Sc’是串行的,称调度Sc为冲突可串行化的调度。若一个调度是冲突可串行化的,则一定是可串行化调度。
例:Sc=r1(A)w1(A)r2(A)w2(A)r1(B)w1(B)r2(B)w2(B)
Sc=r1(A)w1(A)r2(A) r1(B)w1(B) w2(A)r2(B)w2(B)
Sc=r1(A)w1(A) r1(B)w1(B) r2(A)w2(A)r2(B)w2(B)
注:冲突可串行化调度是可串行化调度的充分条件,不是必要条件、还有不满足冲突可串行化条件的可串行化调度。
数据库管理系统如何保证调度是可串行化的呢?
两段锁协议:所有事务必须分成两个阶段对数据项加锁和解锁。
- 在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁;
- 在释放一个封锁之后,事务不在申请和获得任何其他锁。
所谓两段是指,事务分为两个阶段:
第一阶段是获得封锁,也称为扩展阶段;
第二阶段是释放阶段,也称为收缩阶段
结论:如果并发执行的所有事务遵循两段锁协议,那么对这些事务的任何调度都是可串行化调度
什么是多粒度封锁?
封锁的粒度:封锁对象的大小
在一个系统中同时支持多种封锁粒度供不同的事务选择是比较理想的,这种方法称为多粒度封锁。
封锁的对象有哪些?
- 物理单元:页(数据页或索引页)、物理记录等
- 逻辑单元:属性值、属性值的集合、元组、关系、索引项、整个索引、整个数据库
如何进行封锁的?
多粒度树:根节点是整个数据库,表示最大的数据粒度,叶节点表示最小的封锁粒度。
多粒度封锁协议:允许多粒度树中的每个结点被独立的加锁,对每一个结点加锁(显式封锁)意味着这个结点的所有后裔结点也被加以同样类型的锁(隐式封锁)。
对某个数据加锁时,系统要检查该数据对象上有无显示封锁与之冲突,同时还要上下检查是否存在隐式封锁,这样的检查效率太低,因此提出了——意向锁
意向锁:如果对一个结点加意向锁,则说明该结点的下层结点正在被加锁;对任意一结点加锁时,必须先岁它的上层结点加意向锁。
意向锁有哪些种类?
IS锁(意向共享锁):如果对一个数据对象加IS锁,表示它的后裔结点拟(意向)加S锁。
IX锁(意向排他锁):如果对一个数据对象加IX锁,表示它的后裔结点拟(意向)加X锁。
SIX锁(共享意向排他锁):如果对一个数据对象加SIX锁,表示对它加S锁,再加IX锁,即SIX=S+IX。
T2 T1 | S | X | IS | IX | SIX | - |
S | Y | N | Y | N | N | Y |
X | N | N | N | N | N | Y |
IS | Y | N | Y | Y | Y | Y |
IX | N | N | Y | Y | N | Y |
SIX | N | N | Y | N | N |
|
- | Y | Y | Y | Y | Y | Y |
申请封锁时应该按自上而下的次序进行,释放封锁时应该按自下而上的次序进行
晚安\( ̄︶ ̄)↗