数据库系统概论(四)并发控制--隔离等级、封锁、封锁粒度
并发控制
并发控制的任务: 对并发操作进行正确调度(可串行化调度)
保证事务隔离度
保证数据库一致性
并发操作带来的不一致性:丢失修改 不可重复读(包括幻读) 脏读
1.丢失修改
两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的;
2.脏读
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
3.不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。
这样在一个事务内两次读到的数据不一样,因此称为是不可重复读。
4.幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
不可重复读的重点是修改(1读--2修--1读):
同样的条件, 你读取过的数据, 再次读取出来发现值不一样了
幻读的重点在于新增或者删除(1修--2增/删--1读)
同样的条件, 第1次和第2次读出来的记录数不一样
这里,似乎不可重复读和幻影读是一致的,那么它们的区别是什么呢?
从控制的角度来讲,不可重复读只需要锁住满足条件的记录,而幻影读要锁住满足条件的及其相近的记录。所以,避免幻读,必须锁住表,避免不可重复读,只需要锁住行。
参考https://www.iteye.com/blog/uule-1109647
事务隔离等级
未提交读(READ UNCOMMITED):两个事务互相可见,即使一个事务未提交也能获取其使用的数据
已提交读(READ COMMITED):一个事务进行时可见其他已提交的事务
可重复读(REPEATABLE READ):事务进行时,其他所有事务都对其不可见,无法获取其他事务的任何数据,也是InnoDB的默认隔离等级
可串行化(SERIALIZABLE):在读取的每一行数据上都加上锁,会造成大量的锁超时和锁征用,严格保证一致性,不存在并发
封锁
排它锁(Exclusive Lock,X锁):不允许其他事务读、修改、加锁
共享锁(Share Lock,S锁):允许其他事务读A、加S锁,但在S锁释放前不可修改
封锁协议
一级封锁协议:事务在修改数据前必须先加X锁,直到事务结束释放锁
一级封锁防止修改丢失,保证事务的可恢复
由于不修改就不用加锁,不能防止脏读
二级封锁协议:一级封锁协议+ 事务在读取数据前必须加S锁,读完释放
二级封锁避免了修改丢失、脏读,但存在不可重复读
三级封锁协议:一级封锁协议+ 事务在读取数据前必须加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两个事务永远不能结束,形成死锁
预防方法:一次封锁法,必须一次对所有要使用的数据全部加锁,但难确定一个事务会使用到哪些数据
顺序封锁法,对数据对象规定一个封锁顺序,事务按照顺序进行封锁,但维护成本高也难以实现
数据库解决死锁更多是通过
诊断并解除死锁:超时法,一个事务若超时,就认为发生了死锁
但有可能误判长事务或时限设置太长不能及时发现
等待图法,事务等待图中是否存在回路来判断发生死锁,撤销代价最小的事务释放其锁
并发调度的可串行化
一个调度是可串行化的才认为是正确的调度,可串行化就是调度的结果等价于一种(A、B的顺序可能对换)串行执行的结果
冲突可串行化调度
冲突操作指的是不同事物对同一数据的读-写和写-写
在调度序列中,不同事物的冲突操作、同一事务的两个操作,不能交换(SWAP)
今有调度Sc1=r1(A)w1(A)r2(A)w2(A)r1(B)w1(B)r2(B)w2(B)
把w2(A)与r1(B)w1(B)交换,得到:
r1(A)w1(A)r2(A)r1(B)w1(B)w2(A)r2(B)w2(B)
再把r2(A)与r1(B)w1(B)交换:
Sc2=r1(A)w1(A)r1(B)w1(B)r2(A)w2(A)r2(B)w2(B)
Sc2等价于一个串行调度T1,T2,Sc1冲突可串行化的调度
一个调度Sc在保证冲突操作次序不变的情况下,交换不冲突操作得到一个串行调度,那么称其是冲突可串行化的
冲突可串行化是在可串行化上加了条件
有3个事务
T1=W1(Y)W1(X),T2=W2(Y)W2(X),T3=W3(X)
调度L1=W1(Y)W1(X)W2(Y)W2(X) W3(X)是一个串行调度。
调度L2=W1(Y)W2(Y)W2(X)W1(X)W3(X)不满足冲突可串行化。
但是调度L2是可串行化的,因为L2执行的结果与调度L1相同,Y的值都等于T2的值,X的值都等于T3的值
两段锁协议
事务分为两个阶段
①扩展阶段
事务只能申请锁,不能释放锁
②收缩阶段
事务只能释放锁,不能申请锁
遵循两段锁协议必定是可串行化的
两段锁和一次封锁法的区别:
预防死锁的一次封锁法遵守两段锁协议;但是两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁。
原因在于两段锁协议仅是针对一个事务的加锁、释放时期,死锁则是针对两个事务的并发过程中的抢锁问题
封锁粒度
封锁对象的大小称为封锁粒度(Granularity)
逻辑单元:属性值,属性值集,元组,关系,索引项,整个索引,整个数据库
物理单元:页(数据页,索引页),物理记录
封锁粒度与系统的并发度和并发控制的开销密切相关:
1.封锁的粒度越大,数据库所能够封锁的数据单元就越少,并发度就越小,系统开销也越小;
2 .封锁的粒度越小,并发度较高,但系统开销也就越大
选择封锁粒度:
1.需要处理多个关系的大量元组的用户事务:以数据库为封锁单位
2.需要处理大量元组的用户事务:以关系为封锁单元
3.只处理少量元组的用户事务:以元组为封锁单位
多粒度封锁:在一个系统中同时支持多种封锁粒度供事务选择
多粒度树中,根节点是整个数据库,表示最大的数据粒度。叶节点是最小的,如元组、属性值
在多粒度封锁中,一个数据对象能被分为显式封锁(本身直接加锁)和隐式封锁(上级结点加锁)
意向锁(Intention Lock)
在上述多粒度封锁中,对某个数据加锁要检查数据对象本身、上级、下级结点的封锁情况,为了提高检查效率采用意向锁
对一个结点加意向锁,说明该结点下层正在加锁
对任一结点加锁,必须先对其上层结点加意向锁
①意向共享锁(IS)
表示该结点的后裔结点拟(意向)加S锁
②意向排它锁(IX)
表示该节点的后裔结点拟(意向)加X锁
③共享意向排它锁(SIX=S+IX)
表示对其本身加S锁,再加IX表示后裔结点拟加X锁
例如对某个表加SIX锁,则表示该事务要读(S)整个表,同时会更新(IX)个别元组。
锁的强度
指对其他锁的排斥程度
一个事务在申请封锁时,以强锁代替弱锁是安全的,反之不然。
强度排序:
X > SIX > S / IX > IS
具有意向锁的多粒度封锁方法
在具有意向锁的多粒度封锁方法中,任意事务T要对一个数据对象加锁,必须先对它的上层结点加意向锁。
申请封锁时应该按自上而下的次序进行,释放封锁时则应该按自下而上的次序进行。(栈结构)