(十六、十七)数据库并发控制(上)
(十六、十七)数据库并发控制(上)
1. 简介和引入知识
1. 事物
1. 事物的概念
事物这个概念在数据库中可谓是最为常见。它是指一些列操作序列(一个或一个以上)当一个事务被提交给了DBMS(数据库管理系统),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态(要么全执行,要么全都不执行);同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。从事物的概念出发。就可以引出事物的四大特性。
一些形式化的定义为了方便后面的描述
A,B,C来表示数据对象
R(A),W(B) 来表示对于数据对象的读写操作
2. 事物的四大特性
这里感觉cmu ppt里面对于四种特性一句话的概括特别好。
- 原子性(Atomicity): all or nothing
- 一致性(Consistency): it looks correct to me
- 隔离性(Isolation): as if alone
- 持久性(Durability): survive failures
3. 确保原子性的机制
首先考虑这样一个问题。加入我把当前账户的$100取出转账给andy。但是在我们取出它之后,转账给andy之前这个事物突然终止了。或者停电了。这样如果dbms什么都不做,就有100 ¥ 蒸发掉。那么如何解决这一问题。
1. Logging(日志)
dmbs的日志会记录所有的行为。这样就可以当事物被abort的时候撤销这个事物已经执行了的无效行为。几乎所有的DBMS
都是用了这种方法
2. Shadow Paging
在这种机制下DBMS复制所有的page。当事物对这些page进行改变的时候。会改变这些page的副本。只有当这个事物成功commit之后。这些被改变的副本就会对其他用户可见。
2. 两种常见的并发协议和例子
并发协议指的是。dbms如何控制多个事物的交错执行。
两种最常见的协议分别是
- 悲观协议: 从一开始就不要让问题出现。
- 乐观协议: 假设冲突非常少。只有当发生的时候才会解决它
下面来看一些并发交错执行的例子
1. 顺序执行example
Assume at first A and B each have $1000.
左边T1先执行T2后执行整个执行过程大概如下
T1:
A = A - 100 = 900
B = B + 100 = 1100
T2 :
A = A * 1.06 = 954
B = B * 1.06 = 1166
右边则T2先执行。T1后执行。执行的结果和左边是完全一致的
2. INTERLEAVING EXAMPLE (GOOD)
可以发现对于左边。虽然在两个事物之间有交叉。但是最后的结果是一样的。我们说左边这个调度是可序列化的。
3. INTERLEAVING EXAMPLE (BAD)
对于上面的操作。他最后的结果和序列化的结果不一样。则这个调度就是不正确的
3. 几种冲突
发生冲突的操作主要分为下面三大类
- Read-Write Conflicts (R-W)
- Write-Read Conflicts (W-R)
- Write-Write Conflicts (W-W)
1. READ-WRITE CONFLICTS
读写冲突造成的问题就是不可重复读的问题
事物T1先开始执行。首先读出来A的值发现位10。接下来执行权交给了事物T2运行。T2对A进行了写操作。但是对于事物T1而言,它会认为它没有对A进行了修改操作。因此当事物T2提交之后,执行权回到T1的时候,T1再读取A发现读出来的为19。这里就存在这错误。
2. WRITE-READ CONFLICTS
读未提交的数据(脏读)
这里发生的问题就是。事物T1先修改了A的值,但是后面这个事物被abort了。但是T2以为这个值已经被修改了。所以它直接读了T1修改完之后读值进行了操作。
3. WRITE-WRITE CONFLICTS
这里对于T1而言它不知道A被重新写了两次。
2. CONFLICT SERIALIZABILITY INTUITION
首先通过两个例子看一下有冲突的可以序列化的调度。和有冲突不能序列化的调度
冲突的几种情况在上面已经介绍过了
可以发现左边和右边并不等价。
那如何判断一个调度是否是冲突可序列化的那。最常用的算法就是依赖图算法(lab4中也有用到这个)
点:事物来表示点
边:如果事物Ti的一个操作Oi与事物Tj的一个操作Oj发生冲突。并且操作Oi发生在Oj之前。那么存在一条从事物Ti指向事物Tj的边
如果出现环。则无法冲突可序列化
1. 依赖图算法举例
1.1 例子一。😈
1.2 例子2 🌟
- 首先有三个事物。先在图中画出三个点
- 找到不同事物之间的冲突关系。然后得到边
2. VIEW SERIALIZABILITY算法
除了依赖图之外。还有一个算法来判断是否可以冲突序列化
这个算法让我们站到一个更高的层次来判断是否可以序列化。如果两个调度S和S‘满足下面的条件。则我们说这两个调度等价
对于上面这个例子。如果是基于依赖图算法的话。那么它就是一个无法冲突可序列化的调度。但是根据VIEW SERIALIZABILITY算法
。它可以转换成一个可序列化的算法
对于这两个调度。我们站到更高的层次上来看。对于读操作。都是读到最刚开始A的值。对于写操作。写的顺序虽然交换了。但是并不影响结果。
3. 两段锁协议
最经典。也是实验要用到的并发协议。
首先为什么叫两段锁协议那。肯定是因为它有两个阶段吧(:。
阶段1:Growing
这一阶段每个事物需要从DBMS的锁管理处获得锁
阶段2:Shrinking
这一阶段事物只允许释放之前获得的锁。而不允许加上新的锁
在看一个例子之前。先来看一下这个协议中的锁的类型。
两大类的锁分别为排他锁和共享锁
3.1二阶段锁协议的例子
从这个例子来看二阶段锁似乎可以完美的运行。事物T1先开始执行。由于这一事物的操作中有写操作。所以事物T1对A加上排他锁。这样当事物T2开始的时候。由于事物T2也想写A。所以事物T2也去尝试加上排他锁。但是由于A此时有一个排他锁。(事物T1持有的。此时还没有释放)所以事物T2这个时候就需要wait
。等到事物T释放对于A的锁之后。事物T2才能获得对于A的加锁权。随后事物T2对A执行写操作。
但是二阶段锁有他的缺点
比如对于上面这种情况。事物T1先修改了A。但是这个事物后面ABORT
掉了。但是T2并不知道,所以T2这里还是会读出A的值。然后在进行修改。这样就可能产生连锁错误。也就是上面提到的。脏读。所以为了解决这种问题。DBMS会因为T1的abort也把T2 abort掉。
3.2 强限制的2PL
看下面这个例子。有两个事物。分别如下
先看没有2PL的情况
总感觉这里应该是1900。不过肯定有问题就是了。下面看引入2PL。如何解决这一问题
这里符合2PL。加锁和解锁两个阶段完全分开。而且对于有写操作的加X锁。对于只需要读操作的加S锁
严格的2PL协议是说如果这个事物。如果一个value要被一个事物写的话。那么在这个事物完成之前。其他的事物都不允许读或者重写它
这样就可以避免。脏读的出现。
3.4 2PL可能会导致死锁
死锁现象是由于两个事物循环等待锁。导致的
如何探测死锁在上面的依赖图算法有说过
1. 死锁处理 ---> 牺牲者 + 回滚
根据一定的原则选择一个牺牲者。(可以是年龄最小、最大、或者是最近最少使用等等原则)。然后决定回滚多少关于这个事物的改变。
2. 死锁预防
2.1. 当一个事物尝试去获得一个由其他事物持有的锁时。DBMS会选择一个kill掉来防止死锁。
有两种不同的策略来实现这一机制
这是一种基于抢占的机制
这里的优先级就是它开始的时间戳。
[Attention]: 这里的事物如果回滚了。它会以它之前的时间戳重启
用下面这个例子来解释一下上面的策略。
- T1 比T2有更高的优先级。T1这里需要等待T2对于A的锁释放。如果按照
wait-Die
而言。优先级高的要等待优先级低的释放。所以T1就会等待。但是如果按照wait-wait
而言t2就会被aborts掉。
2.2 Lock timeouts
这种机制下。一个事物等待锁的时间是有限制的。如果超时的话它就会回滚和重启。
这种机制非常容易实现。但是问题就是如何界定这个超时时间
3.5 意向锁
1. 为什么需要意向锁
下面来看一下意向锁的作用:参考自https://www.zhihu.com/question/51513268/answer/834445344
事务A锁住了表中的一行,让这一行只能读(数据库自动给该表增加意向共享锁),不能写。之后,事务B申请整个表的写锁。那么事物B会怎么做那。我们来看没有意向锁的
如果没有意向锁则是这样的:
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
如果没有意向锁的话,则需要遍历所有整个表判断是否有行锁的存在,以免发生冲突
那么如果增加了意向锁之后那
step1:判断表是否已被其他事务用表锁锁表
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。因为意向锁的存在代表了,有行级锁的存在或者即将有行级锁的存在。因而无需遍历整个表,即可获取结果。
2. 带有意向锁的加锁协议
首先我们要知道意向锁之间是互相兼容的
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
但是它是和普通的排他/共享锁互斥
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
共享锁(S) | 兼容 | 互斥 |
排他锁(X) | 互斥 | 互斥 |
SIX:
表示这一子树的根结点是被共享锁锁住。而在低层次的结点(比如叶子结点)会加排他锁。
这里附上带有意向锁的兼容矩阵
下面看一个有三个事物的例子
假设按照顺序。T1有更高的优先级
这里由于T1要扫描整个表同时更新一些tuple。所以这里我们要对这个表加SIX
对于事物T2。只读取一个tuple。所以对整个表+ IS锁。同时对自己读区的tuple。加S锁,当然如果这个tuple在之前被T1加了X锁的话则就必须要等待了⌛️
随后对于事物T3。要扫描整个R。就要先等待了。因为整个表还有SIX锁。就表示有某些tuple。处于X锁
这里要等待对于整个表的SIX意向锁释放之后。就可以扫描整个表了。
简而言之意向锁的作用。就是可以实现表及锁和行级锁共存。
- 如果
表X锁
被占有,则其他事务尝试获得IS
、IX
均会阻塞,也就不能继续获取行X锁
或行S锁
了. - 如果
表S锁
被占有,则其他事务可以获得IS
, 而获得IX
会阻塞. 从而阻止其他事务获得行X锁
4. Lock Manager实现补充
1. Lock Table
这一部分是书上的内容。就是讲解锁管理机制是如何管理锁表的。
是利用上图的链式哈希表来实现对于锁得管理。可以看到对于元素I23
事物T1、T8正在持有锁。而事物T2正在等待锁。比如T1、T8正在读该元素因此它们两个都加了S锁。但是T2想要修改这个元素也就是T2想要加X锁。因此这里T2就必须等待T1、T8。S锁的释放。这里也是我们lab4实现整个锁管理的主要结构
2. 插入、删除操作实现
之前我们的注意力都在读操作和写操作上。下面我们把目光聚集到插入和删除等操作上。
我们用\(I_i\) 表示事物Ti的操作。用\(I_j\) 表示事物Tj的操作。Let \(I_i = delete(Q)\)。我们考虑不同种类的\(I_j\)
对于插入操作。我们无法对一个不存在的元素。进行读和写。在2PL协议中。插入操作就可以看成写操作。也就是说可以获得X锁。如果Ti要执行Insert(Q)
操作。那么Ti会给新加入的元素Q一个X锁。