多个事务同时存取共享的数据库时,如何保证数据库的一致性?
一、并发操作与并发问题
- 在多用户DBS中,如果多个用户同时对同一数据操作,称为并发操作。
- 并发操作引发的问题
- 丢失更新
- 脏读
- 不一致分析
二、并发事务调度与可串性
1. 可串化调度
- 调度:多个事务的读写操作按时间排序的执行序列
- 调度中每个事务的读写操作保持原来顺序
- 多个事务的并发执行存在多种调度方式
T1: r1(A) w1(A) r1(B) w1(B)
T2: r2(A) w2(A) r2(B) w2(B)
Sc = r1(A) w1(A) r2(A) w2(A) r1(B) w1(B) r2(B) w2(B)
Sa = r1(A) w1(A) r1(B) w1(B) r2(A) w2(A) r2(B) w2(B)
2. 冲突可串性
如果调度中一对连续操作是冲突的,则意味着如果它们的执行顺序交换,则至少会改变其中一个事务的最终执行结果
下面两个序列结果不同。第一个序列中,r2(B)读的是B的新值;第二个序列中,r2(B)读的是B的旧值。
r1(B) w1(B) r2(B) w2(B)
r1(B) r2(B) w1(B) w2(B)
下面两个序列结果就是相同的。
r1(A) w1(A) r2(B) w2(B)
r1(A) r2(B) w1(A) w2(B)
3. 优先图
优先图用于冲突可串性的判断
优先图结构
- 节点:事务
- 有向边:若Ti —> Tj,那么存在Ti中的操作A1和Tj中的操作A2满足
- A1在A2前
- A1和A2是冲突操作
三、锁与可串性实现
1. 二阶段锁(2PL)
- 事务读写数据前需要获得该数据上的锁。
- 事务释放锁后,将不再获得任何锁。
1. Exclusive Locks(X锁,也称写锁)
事务获得数据的X锁后才能对数据进行修改
2. Share Locks(S锁,也称读锁)
如果数据被加了S锁,那么其他事务无法获得该数据的X锁,但是可以获得S锁。事务获得数据的S锁后,如果需要修改数据,需要通过Update Lock把S锁升级为X锁
3. Update Lock(U锁,也称更新锁)
- 如果事务取得了数据R上的更新锁,则可以读R,并且可以在以后升级为X锁
- 单纯的S锁不能升级为X锁
- 如果事务持有了R上的Update Lock,则其它事务不能得到R上的S锁、X锁以及Update锁
- 如果事务持有了R上的S Lock,则其它事务可以获取R上的Update Lock
2.多粒度锁(MGL)
-
粒度指加锁的数据对象的大小,可以是整个关系、块、元组、整个索引、索引项
-
锁粒度越细,并发度越高;锁粒度越粗,并发度越低
-
允许多粒度树中的每个结点被独立地加S锁或X锁,对某个结点加锁意味着其下层结点也被加了同类型的锁
-
多粒度锁上的两种不同加锁方式
- 显式加锁:应事务的请求直接加到数据对象上的锁
- 隐式加锁:本身没有被显式加锁,但因为其上层结点加了锁而使数据对象被加锁
-
给一个结点显式加锁时必须考虑
- 该结点是否已有不相容的显式锁存在
- 该结点是否已有不相容的隐式锁存在
- 所有下层结点中是否存在不相容的显式锁
理论上要搜索上面全部的可能情况,才能确定P上的锁请求能否成功,显然是低效的,因此引入意向锁。
3.意向锁
- IS锁 Intent Share Lock,意向共享锁,意向读锁
- IX锁 Intent Exclusive Lock,意向排他锁,意向写锁
- 如果对某个结点加IS(IX)锁,则说明事务要对该结点的某个下层结点加S(X)锁
- 对任一结点P加S(X)锁,必须先对从根结点到P的路径上的所有结点加IS(IX)锁
四、事务的隔离级别
- 隔离级别是针对连接(会话)而设置的,不是针对一个事务
- 不同隔离级别影响读操作。
隔离级别 | 脏读 | 不可重复读取 | 幻象 |
未提交读 | ✔ | ✔ | ✔ |
提交读 | ✘ | ✔ | ✔ |
可重复读 | ✘ | ✘ | ✔ |
可串行读 | ✘ | ✘ | ✘ |
1. 未提交读 Read Uncommitted
- 允许读取当前页面上的任何数据,不管是否已经提交。
- 事务不必等待任何锁,也不需要对读取的数据加锁。
- 实际DBMS中一般不使用。
2. 提交读 Read Committed
- 保证事务不会读取到其他未提交事务所修改的数据(可防止脏读)
- 事务必须在所访问数据上加S锁,数据一旦读出,就马上释放持有的S锁
3. 可重复读 Repeatable Read
- 保证事务在事务内部如果重复访问同一数据(记录集),数据不会发生改变。即,事务在访问数据时,其他事务不能修改正在访问的那部分数据
- 可重复读可以防止脏读和不可重复读取,但不能防止幻像
- 事务必须在所访问数据上加S锁,防止其他事务修改数据,而且S锁必须保持到事务结束
4. 可串行读 Serializable Read
- 保证事务调度是可串化的
- 事务在访问数据时,其他事务不能修改数据,也不能插入新元组
- 事务必须在所访问数据上加S锁,防止其他事务修改数据,而且S锁必须保持到事务结束
- 事务还必须锁住访问的整个表
死锁
死锁的两种处理策略
- 死锁检测:检测到死锁,再解锁
- 死锁预防:提前采取措施防止出现死锁
死锁检测
- 超时
- 等待图
- 结点:事务
- 边:Ti->Tj,Ti必须等待Tj释放所持有的某个锁才能继续执行。
- 如果等待图中出现了环路,说明产生了死锁。
死锁预防
1. 按封锁对象的某种优先顺序加锁
- 把要加锁的数据库元素按照某种顺序排序
- 事务只能按照元素顺序申请锁
2. 使用时间戳
- 每个事务开始时赋予一个时间戳
- 如果事务T被Rollback然后再Restart,T的时间戳不变
- Ti请求被Tj持有的锁,根据Ti和Tj的timestamp决定锁的授予
1.等待-死亡
T请求一个被U持有的锁
- 如果T的时间戳小于U,那么T需要等待
- 如果T的时间戳大于U,那么T需要rollback,之后再restart,时间戳和之前相同
2. 伤害-等待
T请求一个被U持有的锁
- 如果T的时间戳小于U,U释放锁,并且rollback和restart
- 如果T的时间戳大于U,那么T需要等待
两种并发控制思路
1. 悲观并发控制
- 立足于事先预防事务冲突
- 采用锁机制实现,事务访问数据前都要申请锁
- 锁机制影响性能,容易带来死锁、活锁等副作用
操作之前先加锁,操作完释放锁,无法避免死锁。适合写频繁的应用场景
2. 乐观并发控制
- 乐观并发控制假定不太可能(但不是不可能)在多个用户间发生资
源冲突,允许不锁定任何资源而执行事务。 - 只有试图更改数据时才检查资源以确定是否发生冲突。如果发生冲突,应用程序必须读取数据并再次尝试进行更改。
- 读阶段—>有效性确认阶段—>写阶段
操作之前不加锁,只有写提交时才检查是否发生了写冲突。适合读频繁的应用场景