并发控制技术是实现事务隔离性以及不同隔离级别的 关键
, 实现方式有很多, 按照其对可能冲突的操作采取的不同策略可以分为 乐观并发控制
和 悲观并发控制
两大类。
乐观并发控制 | 对于并发执行可能冲突的操作, 假定其不会真的冲突, 允许并发执行, 直到真正发生冲突时才去解决冲突, 比如让事务回滚 |
悲观并发控制 | 对于并发执行可能冲突的操作, 假定其必定发生冲突, 通过让事务等待(锁)或者中止(时间戳排序)的方式使并行的操作串行执行 |
基于封锁的并发控制
核心思想:
对于并发可能冲突的操作, 比如读-写, 写-读, 写-写, 通过锁使它们互斥执行。锁通常分为 共享锁
和 排他锁
两种类型。
共享锁(S) | 事务T对数据A加共享锁, 其他事务只能对A加共享锁但不能加排他锁 |
排他锁(X) | 事务T对数据A加排他锁, 其他事务对A既不能加共享锁也不能加排他锁 |
基于锁的并发控制流程:
锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁
可能出现的问题:
死锁 | 饥饿 |
多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行 | 数据项A一直被加共享锁, 导致事务一直无法获取A的排他锁 |
对于可能发生冲突的并发操作, 锁使它们由并行变为串行执行, 是一种 悲观的并发控制
。
基于时间戳的并发控制
核心思想:
对于并发可能冲突的操作, 基于时间戳排序规则选定某事务继续执行, 其他事务回滚。
系统会在每个事务开始时赋予其一个时间戳, 这个时间戳可以是系统时钟也可以是一个不断累加的计数器值, 当事务回滚时会为其赋予一个新的时间戳, 先开始的事务时间戳小于后开始事务的时间戳。
每一个数据项Q有两个时间戳相关的字段:
W-timestamp(Q):
成功执行write(Q)的所有事务的最大时间戳R-timestamp(Q):
成功执行read(Q)的所有事务的最大时间戳
时间戳排序流程:
- 读取资源
- 写入资源
基于时间戳排序和基于锁实现的本质一样: 对于可能冲突的并发操作, 以串行的方式取代并发执行, 因而它也是一种 悲观并发控制
。它们的区别主要有两点:
- 基于锁是让冲突的事务进行
等待
, 而基于时间戳排序是让冲突的事务回滚
。 - 基于锁冲突事务的执行次序是根据它们申请锁的顺序,
先申请的先执行
; 而基于时间戳排序是根据特定的时间戳排序规则
。
基于有效性检查的并发控制
核心思想:
事务对数据的更新首先在自己的工作空间进行, 等到要写回数据库时才进行有效性检查, 对不符合要求的事务进行回滚。
基于有效性检查的事务执行过程会被分为三个阶段:
读阶段:
数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行, 并不对数据库进行真正的更新。有效性检查阶段:
对事务进行有效性检查, 判断是否可以执行write操作而不违反可串行性。如果失败, 则回滚该事务。写阶段:
事务已通过有效性检查, 则将临时变量中的结果更新到数据库中。
有效性检查通常也是通过对事务的时间戳进行比较完成的, 不过和基于时间戳排序的规则不一样。该方法允许可能冲突的操作并发执行, 因为每个事务操作的都是自己工作空间的局部变量, 直到有效性检查阶段发现了冲突才回滚。因而这是一种 乐观的并发策略
。
基于快照隔离的并发控制
快照隔离是 多版本并发控制(mvcc)
的一种实现方式。
其 核心思想是:
数据库为每个数据项维护多个版本(快照), 每个事务只对属于自己的私有快照进行更新, 在事务真正提交前进行有效性检查, 使得事务正常提交更新或者失败回滚。
由于快照隔离导致事务看不到其他事务对数据项的更新, 为了避免出现丢失更新问题, 可以采用以下两种方案避免:
先提交者获胜:
对于执行该检查的事务T, 判断是否有其他事务已经将更新写入数据库, 是则T回滚否则T正常提交。先更新者获胜:
通过锁机制保证第一个获得锁的事务提交其更新, 之后试图更新的事务中止。
事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发控制
。