本章以及上一章的并发控制都是关于事务处理技术的。
事务是一系列的数据库操作,是数据库应用程序的基本逻辑单元。
事务处理技术主要包括
- 数据库恢复技术(上一章)
- 并发控制技术
一、并发事务带来的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。
1. 脏读(Dirty read)
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是 “脏数据” ,依据 “脏数据” 所做的操作可能是不正确的。
(T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。)
2. 丢失修改(Lost update)
指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
(T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。)
3. 不可重复读(no-repeatable read)
一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
(T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同)
4. 幻读(Phantom read)
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
(T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同)
⚠ 不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):
事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):
假如某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
二、封锁
并发控制的主要技术有封锁 locking、时间戳 timestamp、乐观控制法 optimistic scheduler 和多版本控制 MVCC 等
封锁是众多数据库产品采用的基本方法。
所谓封锁就是事务T在对某个数据对象例如表、记录等操作之前,先向系统发出请求,对其加锁,在事务T释放它的锁之前,其他事务不更新此对象。
确切的控制由封锁的类型决定,如下
1. 封锁类型
基本的封锁类型有两种:排他锁 X 锁 和 共享锁 S锁
① 排他锁 - X 锁/写锁
一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和修改。
加X锁期间其它事务不能对 A 加任何锁。这就保证了其他事务在该事务释放X锁之前不能读取和修改A
② 共享锁 - S 锁/读锁
一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。
加S锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。这就保证了其他事务可以读A,但在该事务释放S锁之前不能对A进行修改
③ 数据锁相容矩阵
2. 封锁协议 - 三级封锁协议
在运用X锁和S锁对数据对象加锁的时候,还需要约定一些规则。比如何时申请X锁或S锁、持锁时间、何时释放等。这些规则称为封锁协议。此处介绍的是三级封锁协议,后续还有两段锁协议
① 一级封锁协议
事务 T 要修改数据 A 时必须加 X 锁
,直到 T 结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
但不能解决读脏数据和不可重复读的问题,因为在一级封锁协议中,仅仅读数据而对其进行修改是不需要进行加锁的
② 二级封锁协议
在一级的基础上,要求读取数据 A 时必须加 S 锁, 读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
但不能解决不可重复读问题,因为读完数据后就释放S锁,其他事务可以再加锁进行修改
③ 三级封锁协议
在一级协议的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁
。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
④ 三级封锁协议总结
三、活锁和死锁
和操作系统一样,封锁的方法可能引起活锁和死锁问题
1. 活锁
避免活锁的方法就是采用先来先服务的策略。
2. 死锁
事务T1封锁数据R1,事务T2封锁数据R2,T1请求R2, T2请求R1,于是事务T1等待事务T2释放锁,事务T2也等待事务T1释放锁,两个事务循环等待,永远不能结束。如上图 b 所示
3. 死锁的处理和预防
对于死锁问题,要么采取措施预防死锁发生,要么允许死锁发生,检测到死锁后采取策略解除死锁
① 死锁的预防
破坏产生死锁的条件
一次封锁法
每个事务必须一次性将所有需要的数据全部加锁,否则不能执行
顺序封锁法
预先对数据对象规定一个封锁顺序,所有事务都按照整个顺序进行封锁
② 死锁的检测和处理
超时法
如果一个事务的等待时间超过了规定的时限,就认为发生了死锁
等待图法
事务等待图是一个有向图 G = (T, U) ,T是结点的集合,每个结点表示正在运行的事务;U为边的集合,每条边表示事务等待的情况,T1——>T2 表示 T1 正在等待 T2.
如果图中存在回路,则表示系统中出现了死锁
数据库检测到死锁后,一般采取的死锁解除策略是:选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,使其他事务得以继续运行下去。
四、并发调度的可串行性
数据库管理系统对并发事务不同的调度可能会产生不同的结果,只有串行调度才能得到正确的结果
1. 可串行化调度
多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同,称这种调度策略为 可串行 serializable 调度
。
一个给定的并发调度,当且仅当它是可串行化的,才认为是正确调度
示例:
2. 冲突可串行化调度
冲突操作是指不同的事务对同一个数据的读写操作和写写操作
不同事务或者同一事务的冲突操作时不能交换的。
一个调度在保证冲突操作次序不变的情况下,通过交换两个事务不冲突操作的次序得到另一个调度B,则称调度B是冲突可串行化
的调度。
若一个调度是冲突可串行化调度,那么一定是可串行化调度
五、两段锁协议
目前数据库管理系统普遍采用 两段锁 TwoPhase Locking 协议
(简称 2PL)的的方法实现并发调度的可串行性,从而保证调度的正确性
两段锁协议就是指所有事务必须分两个阶段对数据项进行加锁和解锁
- 扩展阶段:在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁
- 收缩阶段:在释放一个封锁的时候,事务不再申请和获得任何其他锁
六、封锁的粒度
封锁对象的大小称为 封锁粒度 granularity
MySQL 中提供了两种封锁粒度:行级锁
以及 表级锁
。
- 表级锁: MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
- 行级锁: MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。
因此如果在一个系统中同时支持多种封锁粒度供不同的事务选择是比较理想的,这种封锁方法称为 多粒度封锁 multiple granularity locking
1. 多粒度封锁
首先我们需要知道多粒度树
:多粒度树的根节点是整个数据库,表示最大的数据粒度,叶结点表示最小的数据粒度
下图给出了一个三级粒度树
多粒度封锁协议允许多粒度树中的每个结点被独立的加锁,对一个结点加锁意味着这个结点的所有后裔结点都被加以同样的锁
显示封锁
:应事务的要求直接加到数据对象上的锁隐式封锁
:该数据对象没有被独立加锁,继承上级结点的锁
系统检查封锁冲突时不仅要检查显示封锁,还要检查隐式封锁。
显然,这样的检查方法效率很低,为此人们引进了意向锁
2. 意向锁
意向锁表示如果对一个结点加锁,则说明该结点的下层结点正在被加锁;对任一结点加锁时,必须先对它的上层结点加意向锁
例如:对任一元组加锁时,必须先对它所在的关系或者数据库加意向锁
下面介绍三种常用的意向锁
① IS 锁
如果对一个数据对象加 IS 锁,则表示它的后裔结点想要加 S 锁
② IX 锁
如果对一个数据对象加 IX 锁,则表示它的后裔结点想要加 X 锁
③ SIX 锁
如果对一个数据对象加 SIX 锁,则表示对他加 S 锁,再加 IX 锁
例如对某个表加 SIX 锁,则表示该事务先要读整个表,读表过程中不允许其他事务进行修改;读表的同时还会对该表中的个别元组进行修改,所以加 IX 锁,表示表下面的元组想要加X锁。
④ 数据锁相容矩阵
从上图我们可以看出锁的强弱程度,即对其他锁的排斥程度。一个事务在申请封锁的时候,以强锁代替弱锁时安全的,反之则不然
总结如下:
- X 锁 不兼容任何锁
- 任意IS / IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁
- 这里兼容关系针对的是表级锁,而
表级的 IX 锁和行级的 X 锁兼容
,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)
七、事务的隔离级别
事务具有隔离性,理论上说事务之间的执行不应该相互影响,其读数据库的影响应该和他们串行时执行一样。
完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上会对隔离性的要求会有所放松。
SQL 标准为事务定义了四个不同的隔离级别,从低到高依次是:
READ-UNCOMMITTED(读取未提交)
:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交)
:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读)
:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化)
:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
(该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。)
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化) 隔离级别。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化) 隔离级别。
八、DBMS保证事务的ACID特性原理
结合上一章和本章的内容,我们来总结一下数据库管理系统是如何保证事务的ACID特性的。
首先,事务的原子性、持久性、隔离性都是为了实现事务的一致性
1. 原子性实现原理 - Undo Log
为了实现原子性,需要通过日志:将所有对数据更新操作都写入日志,如果一个事务中的一部分已经操作成功,但以后的操作由于断电/系统崩溃/其他软硬件错误或者用户提交了rollback 导致无法进行,则通过回溯日志,将已经执行成功的操作撤销 undo,从而达到全部操作失败的目的,使得数据库恢复到一致性的状态,可以继续被使用。
2. 持久性实现原理 - Redo Log
和Undo Log 相反,Redo(重做) Log 记录的是新数据的备份。在事务提交前,只是将Redo Log 持久化即可,不需要数据持久化。当系统崩溃时,虽然数据没有持久化,但Redo Log 已经持久化了。系统可以根据Redo Log 将数据更新到最新的状态。
3. 隔离性实现原理 - 锁
当然,保证事务的隔离性,即并发控制不止可用封锁协议,还有时间戳、多版本控制等等。
基于锁的并发控制流程:
- 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排它锁)。
- 申请锁的请求被发给锁管理器。锁管理器根据当前页是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
- 若锁被授予,则申请锁的事务可以被继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其它事务释放。
可能出现的问题:
- 死锁:多个事务持有锁并循环等待其它事务的锁导致所有的事务都无法继续执行。