并发(同时)控制学习内容主线
引言
1、多个事务串行执行。
执行方式:第一个事务做完——》第二个事务...
优点:调度简单(按一定次序执行),事务之间不会互相干扰,不会破坏事务的ACID特性。
缺点:不能充分利用资源
2、多个事务并发执行。
→交叉并发方式(单CPU)
→同时并发方式(多CPU)
本章讨论的数据库系统并发控制技术是以单处理机系统为基础的交叉并发方式。
事务并发执行可能带来的数据不一致问题
并发操作带来数据不一致性主要包括:
→丢失修改:
T1事务获取数据为16,T2事务获取数据为16,T1将数据-1为15,而T2的值仍为16,所以T2将获取的数据-1后仍为15
→不可重复读:
T1事务没有处理完,T2事务将数据修改后,T1数据查询并使用了修改后的事务
→读“脏”数据:
T1事务修改了数据的值:200,T2获取T1的值:200,此时T1出现故障,数据恢复为100,而此时,T2获取数据的值为200,与此时数据的值不符。
1、丢失修改。以飞机订票为例
T甲 售票点 | T乙售票点 |
时刻1:R(A)=16(即:有16张票) | |
时刻2 | R(A)=16 |
时刻3: A=A-1 W(A)=15 卖出一张票(将剩下的票写回去) | |
时刻4 | A=A-1 W(A)=15 卖出一张票(将剩下的票写回去) |
数据覆盖导致丢失修改甲事务的执行被乙事务干扰,破坏了事务的隔离性
2、不可重复读(不可重复获取数据)
T1 事务(R为读) | T2事务 |
时刻1: R(A)=50 R(B)=100 求和=150 | |
时刻2: | R(B)=100 B=B*2 W(B)=200 |
时刻3: R(A)=50 R(B)=200(重复了) 求和=250 (验算不对) |
破坏了事务的隔离性
3、读“脏”数据
T1 事务(R为读) | T2事务 |
时刻1: R(C)=100 C=C*2 W(C)=200 | |
时刻2: | R(C)=200 |
时刻3: 出现故障,回滚数据 ROLLBACK C恢复为100 |
破坏了事务的隔离性
产生上述三类数据不一致性的主要原因是并发操作破坏了事务的隔离性。并发控制就是要用正确的方式调度并发操作,使一个用户事务的执行不受其他事务的干扰,从而避免造成数据的不一致性。
解决办法:封锁
封锁
1、封锁概念
封锁就是事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。
2、基本的封锁类型有两种
排他锁(简称x锁):
排他锁又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。 (加锁事务可读可改,其它事务不可读不可改)
共享锁(简称S锁):
共享锁又称为读锁,若事务T对数据对象A加上S锁,则其它事务只能再对A加s锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的s锁之前不能对A作任何修改。(加锁事务可读可改,其它事务可读不可改)
封锁协议:
在运用x锁和S锁这两种基本封锁对数据对象加锁时,还需要约定一些规则。如,申请什么样的锁,持锁时间,何时释放等。这些规则称为封锁协议(协议为规则的集合)。
→一级封锁协议:
事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。(解决丢失修改问题)
→二级封锁协议:
在一级封锁协议基础上增加事务T在读取数据R之前必须先对其加s锁,读完后即释放s锁。(解决读脏数据问题)
→三级封锁协议:
在一级封锁协议的基础上增加事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。(解决不可重复读问题)
3、用封锁机制解决丢失修改、不可重复读和读“脏”数据问题
(1)解决丢失修改问题
T1 | T2 |
①Xlock A (事务T1获得对数据A的排他锁) | |
② R(A)=16 (读取A数据) | |
③ | Xlock A (事务T2对数据A申请加排他锁, 此时事务T1正在使用排他锁, 所以T2事务只能等待) |
④A←A-1 W(A)=15 (写入数据) 事务T1对数据A进行修改 | 等待 |
Commit (提交事务) | 等待 |
Unlock A (事务T1释放排他锁) | 等待 |
⑤ | 获得Xlock A (T2事务获得A的排他锁) |
R(A)=15 (T2事务读取得A的值为15) | |
⑥ | A←A-1 对A进行操作 W(A)=14 将数据写入A |
Commit (提交事务T2) | |
Unlock A (释放排他锁) |
(2)解决不可重复读问题
T1事务(只读取数据) | T2事务(读/写数据) |
①Slock A Slock B (申请对A和B加共享锁) | |
R(A)=50 R(B)=100 求和=150 | |
② | Xlock B (T2申请排他锁) |
③R(A)=50 R(B)=100 求和=150 | 等待 (数据B,已经被T1加了共享锁) |
Commit (提交T1事务) | 等待 |
Unlock A Unlock B (释放A和B数据上的共享锁) | 等待 |
④ | 获得XlockB |
R(B)=100 | |
B←B*2 | |
⑤ | W(B)=200 (将修改后的B数据写入) |
Commit | |
Unlock B (释放B上的排他锁) |
(3)解决读“脏”数据问题
T1 (读写数据C) | T2(只读取数据C) |
①Xlock C (T1事务申请为数据C加排他锁(x锁)) | |
R(C)=100 (T1获取C的值) | |
C←C*2 W(C)=200 (T1事务写入C的值) | |
② | Slock C (申请共享锁(s锁)) |
等待 (数据C已经加入排他锁) | |
③ROLLBACK (事务T1出现故障,数据回滚) | 等待 |
回滚结果:c恢复为100 | 等待 |
Unlock C (释放排他锁) | 等待 |
④ | 获得SlockC (获得共享锁) |
⑤ | R(C)=100 (事务T2获取共享锁) |
Commit Unlock C |
活锁和死锁
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 | 等待 | 等待 | Lock R |
等待 | Lock R | 等待 | |
等待 | Unlock | 等待 | |
等待 | 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)死锁的预防(弊端较大,不宜采用)
① 一次封锁法(每个事务一次将要使用的数据全部加锁:封锁范围大,并发范围小,难以确定对象)
② 顺序封锁法(预先对数据对象规定一个封锁顺序:封锁数据对象多,且变化多,维护成本高,难以确定封锁的对象和顺序)
死锁的预防总结:
预防死锁的策略并不很适合数据库的特点,因此DBMS在解决死锁的问题上普遍采用的是诊断并解除死锁的方法。
(2)死锁的诊断与解除
→超时法:
如果一个事务的等待时间超过了规定的时限,就认为发生了死锁。
优点:实现简单。
缺点;
①有可能误判死锁。
事务因为其他原因使等待时间超过时限,系统会误认为发生了死锁。
②时限若设置得太长,死锁发生后不能及时发现。
等待图法(大部分使用)
事务等待图是一个有向图G=(T,U)。
T为结点的集合,每个结点表示正运行的事务:
U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1,T2之间划一条有向边,从T1指向T2。
事务等待图动态地反映了所有事务的等待情况。并发控制子系统周期性地(比如每隔数秒)生成事务等待图,并进行检测。如果发现图中存在回路,则表示系统中出现了死锁。
DBMS的并发控制子系统一旦检测到系统中存在死锁,就要设法解除。通常采用的方法是选择一个处理死锁代价最小的事务,将其撤消,释放此事务持有的所有的锁,使其它事务得以继续运行下去。