# DB 恶补笔记
# 并发控制
# 一致性问题
# 丢失修改(Lost Update)
T1 和 T2 读入同一数据并修改,T2 的提交结果破坏了 T1 提交的结果,导致 T1 的修改被丢失。
# 不可重复读(Non-repeatable Read)
T1 读取数据后,事务 T2 执行更新操作,使 T1 无法再现前一次读取结果
另外情况:
- “读 - 删除”:T2 在 T1 读取数据之后进行了删除
- “读 - 插入”:T2 在 T1 选取数据之后进行插入,导致 T1 再次按条件选取数据记录增多 ——“幻影现象(Phantom Row)”
# 读 “脏” 数据(Dirty Read)
T1 先读取数据并写入,T2 读取统一数据之后,T1 执行撤销,导致 T2 持有的数据不一致
# 封锁
两种基本封锁:
- 排他锁:Exclusive Locks,简记为 X 锁,写锁
- 共享锁:Share Locks,简记为 S 锁,读锁
相容性:
# 封锁协议
三级封锁协议:
- 一级:
- 事务 T 在修改数据 R 之前必须先对其加 X 锁,直到事务结束才释放
- 可防止丢失修改,保证事务 T 是可恢复的
- 二级:
- 一级封锁协议加上事务 T 在读取数据 R 之前必须先对其加 S 锁,读完后即可释放 S 锁
- 可以防止丢失修改和读 “脏” 数据
- 三级:
- 一级封锁协议加上事务 T 在读取数据 R 之前必须先对其 加 S 锁,直到事务结束才释放
- 可防止丢失修改、读脏数据和不可重复读
Q: 如果在一级封锁协议中,将写锁改为短锁,是否能防止丢失修改?
A: 不能。若写锁改为短锁,那么在事务 T1 提交前就释放了锁,写入操作还没有持久化,因此后续事务 T2 得到的仍然是修改前的值,T1 修改丢失。
# 活锁和死锁
活锁:
也就是饥饿现象,因过多事务请求同一数据对象,导致某一事务等待时间过长。
避免方法:先来先服务策略
死锁:
- 死锁预防:
- 一次封锁法:
- 要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行
- 过早加锁,降低系统并发度;难于事先精确确定封锁对象
- 顺序封锁法:
- 预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁
- 成本很高,难以实现
- 一次封锁法:
- 死锁诊断:
- 超时法:
- 如果一个事务的等待时间超过了规定的时限,就认为发生了死锁
- 实现简单;有可能误判死锁;时限若设置得太长,死锁发生后不能及时发现
- 等待图法:
- 若 T1 等待 T2,则 T1,T2 之间划一条有向边,从 T1 指向 T2
- 并发控制子系统周期性地(比如每隔数秒)生成事务等待图,检测事务。如果发现图中存在回路,则表示系统中出现了死锁
- 超时法:
- 死锁解除:
- 选择一个处理死锁代价最小的事务,将其撤消。释放此事务持有的所有的锁,使其它事务能继续运行下去
# 两段锁协议
- 第一阶段是获得封锁,也称为扩展阶段
- 事务可以申请获得任何数据项上的任何类型的锁,但是不能释放任何锁
- 第二阶段是释放封锁,也称为收缩阶段
- 事务可以释放任何数据项上的任何类型的锁,但是不能再申请任何锁
两段锁协议与防止死锁的一次封锁法:
- 一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行,因此一次封锁法遵守两段锁协议
- 但是两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁
满足三级封锁协议一定满足两段锁协议。
满足两段锁协议不一定满足三级封锁协议:两段锁协议在释放阶段,仍然可以穿插其他命令,不需要在事务结束后一次性施放。
# 可串行性
多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同。
冲突可串行化:
- 冲突操作:是指不同的事务对同一数据的读写操作和写写操作
- 不能交换的动作:同一事务的两个操作、不同事务的冲突操作
- 冲突可串行化的调度:在保证冲突操作的次序不变的情况下,通过交换操作得到一个可串行化调度
若一个调度是冲突可串行化,则一定是可串行化的调度。
判断冲突可串行化(优先图):
- 对于每个数据对象建立事务之间的优先图, 当且仅当:
- pi (A),qj (A) 涉及同一数据库元素
- pi (A)<qj (A)(这里指的是调度的先后顺序)
- pi, qj 至少一个是写动作
- 将所有数据对象的优先图的边集进行合并
- 如果存在环,那就不是冲突可串行的;否则,就是冲突可串行的