多版本控制MVCC
调度
冲突串行化
在中 ACID 的 C ,指的是数据库中记录或映射的数据与真实世界中要一致,通俗一点说就是准确。事务的原子性,隔离性和持久性最终的目的都是为了数据的准确。
串行化是保证数据库一致性的最高级别。但是为了获得更好的数据库性能,多个事务需要同时执行。这时数据库的调度显得尤为重要,也就是数据库的并发控制。
串行化是指同一个事务的操作被同时调度,同一时间内只执行同一事务内的操作。
- 如果两个事务间存在对同一数据项的写操作(write),那么认为这俩事务是冲突的,那么不同的调度顺序就会产生不同的结果,那么必然存在着不一致的情况存在。
- 事务间不冲突的操作,其先后执行的顺序是不重要的。
- 如果调度S通过调整不冲突的操作成为 S’,那么调度S和 S’冲突等价的。
- 如果调度 S’是串行化的,那么认为调度 S 是冲突可串行化的
- 通俗说来,就是调度 S 经过一系列等价的调整事务执行次序,最终得到一个串行化的调度。因为串行化调度是能够保证数据一致性的,因此 S 也是能够保证数据一致性的。
可恢复调度
如果,事务 T_1依赖 T_2,读取 T_2中的未提交到数据(未提交读),T_1提交之后,T_2还处于活跃状态。此时如果 T_2出现了问题需要回滚到最初状态,可是 T_1已经提交,所以此次的调度属于不可恢复调度。那么只要依赖的事务后于被依赖的事务提交,即可做到可回复调度。
无级联调度
虽然事务做到了可回复,但是如果发生了回滚,相关依赖的事务都要进行回滚,如果依赖事务很对便会产生大量的级联回滚。因此需要去掉事务间的依赖关系,也就是无级联调度。如果事务 T_1 读取先前 T_2说写的数据,那么只要 T_2 在 T_1读取操作前提交,这样 T_1和 T_2之间便没有了依赖。
隔离级别
- 串行化 事务最准确的执行调度,完全的隔离。
- 可重复读 读取已经提交的数据,同时不受事务期间其它事务提交的影响,因此容易在提交事务时候与最新的数据库状态有冲突,于会产生幻读。
- 已提交读 只能读取已经提交的数据,收到其它事务提交的影响,可能同一事务读取多次同一个数据,等到不一样的结果。
- 未提交读 事务更改的数据,在其它事务中都可以看到,事务之间几乎没有隔离。 级别越高性能越低,准确性越高。准确高性能是每个数据库设计者的努力方向。MVCC 就是其中一个。
时间戳协议
当事务并发执行,数据发生冲突的情况下,如何解决冲突是这些协议的根本目的。
首先我们引入以下相关的变量
- 对于事务 T,我们把一个唯一的时间戳与它联系起来,记为 TS(T)。
- 对于数据项 Q,W-ts(Q)表示执行 Write 操作的最大时间戳。R-ts(Q)表示执行 Read操作的最大时间戳。
时间戳顺序协议(Timestamp-ordering protocol)
- 事务T发出read(Q)操作,如果 TS(T)<W-ts(Q)说明 T 想读的 Q 已经被覆盖,那么 read 操作应该被拒绝,T 事务进行回滚操作。
- 事务T发出read(Q)操作,如果 TS(T)>=W-ts(Q),执行 read 操作,并且将 R-ts(Q)设为 TS(T)
- 事务T发出write(Q)操作,如果 TS(T)<R-ts(Q) 或者 TS(T)<W-ts(Q) 说明 T 产生的 Q 值已经过时,操作终止,T 事务回滚
- 其它情况,事务T发出write(Q)操作,执行 write 操作,并且将W-ts(Q)设为 TS(T) 该协议保证了无死锁,同时确保了冲突可串行化,因为冲突都会按照时间戳的顺序,采用新值,回滚旧值来处理。但是这里存在一个问题,如果有大量短事务与一个长事务冲突,可能会导致长事务反复重启,从而产生事务饥饿的现象。
MVCC
MVCC 是由时间戳顺序协议(Timestamp-ordering protocol)扩展而来。目前绝大多数并发控制机制,要么延迟操作要么终止操作来确保可串行性。如果系统仍然保留历史旧值,就可以避免这样的问题。 因此MVCC的核心是非常朴素的。每个满足条件的 write(Q) 操作会创建一个Q 的新版本,而 read(Q)操作会选择一个合适的 Q 的版本进行读取。 在介绍 MVCC 之前,需要引入几个变量
- 数据项 Q,有<Q_1,Q_2…Q_k>与之关联。
- 每个版本 Q 中包含具体内容 Content,W-ts(Q_k)是Q_k 的创建时间戳。R-ts(Q)是 Q_k的最大读取时间戳。
下面是 MVCC 的运作办法
- 如何事务 T 发出 read(Q),那么返回相应 TS(T)的 Q版本 的content
- 如果事务 T 发出 write(Q),如果 TS(T)>=W-ts(Q_k),而TS(T)<R-ts(Q) ,数据写入太迟,回滚T
- 如果事务 T 发出 write(Q),如果 TS(T)==W-ts(Q_k),覆盖 Q_k
- 如果事务 T 发出 write(Q),如果 TS(T)>=W-ts(Q_k),而TS(T)>R-ts(Q) ,创建新的版本
- Q_k和 Q_j 两个版本的 W-ts 都小于系统最老事务时间戳,那么 Q_k和 Q_j 中的老版本即可删除。这一点很好理解,系统最老的事务都比 Q_k和 Q_j 年轻,那么 Q_k和 Q_j 中年老的就永远不会被读到了。
MVCC 的读取请求永远不会失败,这一点对于实际生产环境中读取远大于写入到数据库非常重要。但是这里也有一个缺点,每次读取都会更新 R-ts因此会产生至少两次磁盘操作。