代码改变世界

事务原理-2

2018-01-11 21:23  Gizing  阅读(1313)  评论(0编辑  收藏  举报

1. 隔离级别

SQL标准定义的四种隔离级别:read uncommittedread committedrepeatable readserializable

隔离级别 实现方式 不能解决的问题
serializable 添加范围锁直到事务T1结束,以阻止其他事务对限定范围内数据的写操作
repeatable read 对读出的记录添加共享锁直至事务T1结束,其他事务对其修改须等到T1结束,但允许其他事务读取 范围查询时可能发生幻读
read committed 事务T1对读出的记录添加共享锁,读取结束释放,其他事务即可修改,T1的不同阶段可能读取到不同结果 不可重复读
read uncommitted 不添加共享锁 脏读

1.1 快照隔离

快照隔离不是一种隔离级别,只是一种隔离技术

  • 使用快照隔离技术的事务中所有读操作,读到的数据一定是一致的
  • 避免了各种读异常现象
  • 如果没有写写冲突则会提交成功,并发效率更高
  • 第1和第3点得以保证,因为事务开始时,处于当时的并发事务的状态(多个事务的状态称为快照)被保存,利用快照可以确定本事务和其他事务间的启动顺序,事务读写数据情况,以确定是否存在写写冲突
  • 并发同时写同一个数据项的事务遵循first committer wins,即只能有一个成功,另外一个必须回滚,相当于没有并发,解决写写冲突

引发的问题:写偏序
解决方法:采用serializable snapshot isolationwrite-snapshot isolation技术

PostgreSQL中快照隔离被称为“可串行化隔离级别”是错误的

PG中使用 "transaction snapshot"表示快照隔离;MySQL中使用 "read view"表示快照隔离

2.并发控制

以下的并发控制方法只涉及serializable隔离级别

2.1 基于锁的并发控制方法

多粒度封锁解决一种锁并发效率低下的问题。两阶段加锁协议解决引入多粒度封锁之后无法保证可串行化问题

锁的加锁范围影响并发度,范围越大并发度越低

2.1.1 锁粒度

最简单的锁是分为两种粒度,即读锁(共享锁)和写锁(排它锁),还可以将锁种类进一步细化,增加意向锁等。
封锁机制能够得到两张表,具体的表内容根据锁粒度有所不同:

  • 锁的相容性矩阵表。用于多个事务之间进行数据项加锁时可以互斥操作
  • 锁的升级表。用于一个事务内对本事务的加锁操作进行锁升级

2.1.2 两阶段加锁协议(two-phase locking protocol, 2PL)

每个事务分为两个阶段:

  • 增长阶段:第一个阶段,事务可以获得锁,但不能释放锁
  • 缩减阶段:第二个阶段,事务可以释放锁,不能获得锁

两阶段加锁协议可以保证可串行性,但不满足可恢复性,因此不能避免级联回滚,不能保证数据一致性。于是2PL有新的变种:

  • 严格两阶段加锁协议(strict 2PL, S2PL):排它锁必须在事务提交后才能释放,避免级联回滚
  • 强两阶段加锁协议(strong 2PL, SS2PL):事务提交之前不释放任何锁

只有SS2PL才能保证数据一致性,这也是主流数据库实现的两阶段加锁协议。(SS2PL+MVCC)

不会发生脏读和不可重复读异常。可能发生幻读异常,分情况讨论:

  • 没有索引,使用表或页面锁:锁的粒度大,其他事务的where条件不能获取被锁住的表或页面,可以避免幻读
  • 没有索引,使用元组级的锁:其他事务的where条件不受加锁的影响,可能存在幻读
  • 有索引:在索引上采用谓词锁解决幻读。谓词锁是专门用于避免幻读的技术,将锁加在条件上而不是元组或表,构成一个条件锁表,其他事务的where条件受条件锁表的影响,不能获取被锁住的条件

2.1.3 死锁

死锁四个必要条件

  • 互斥使用:资源同时刻只能一个进程使用
  • 持有和等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 非抢占分配:已经分配的资源不能被抢占
  • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系

解决办法:

  • 死锁预防:
  • 死锁检测+死锁恢复:
  • 死锁避免:

2.2 基于时间戳的并发控制方法

注意和基于MVCC的时间戳方法区别,这个方法的数据没有多版本

核心:根据事务开始时的时间戳和其他事务读写操作的时间戳做比较决定冲突时事务该如何处理

事务Ti的时间戳TS(Ti)早于事务Tj的时间戳TS(Tj),表示为TS(Ti) < TS(Tj)

  • TS(Ti):事务Ti开始时的时间戳
  • Read(X)-TS(Ti):事务Ti在数据项X上读操作的时间
  • Write(X)-TS(Ti):Ti在X上写操作的时间
  • Commit(X)-TS(Ti):Ti在X上的写操作发生后,Ti是否提交

2.2.1 读写冲突

以下3种情况发生读写冲突及解决方式:

  • 不管TS(Ti) > TS(Tj)或TS(Ti) < TS(Tj),R(X)-TS(Ti) > W(X)-TS(Tj)则读操作被拒绝,Ti被回滚(Ti被中止,然后被重启并重新给TS(Ti)赋予新值),解决读写冲突

  • TS(Ti) >= W(X)-TS(Tj),即事务Ti开始的时间戳比数据项X上的写操作晚,则Ti执行读操作更是发生在后,如果Tj被回滚,则Ti出现脏读,这是不允许的,因此处理方式如下:

    • 如果Commit(X)-TS(Tj)值为true,则Ti的读操作被允许
    • 否则延迟Ti的读操作,意味着读操作被阻塞
  • R(X)-TS(Tj) < W(X)-TS(Ti),则回滚Ti

2.2.2 写写冲突

  • TS(Ti) < W(X)-TS(Tj) 且 W(X)-TS(Tj) < W(X)-TS(Ti),则回滚Ti

2.2.3 总结(晚来的基本被回滚)

  • 写-读冲突:写前读后,事务早于写操作,回滚读并重启;事务晚于写操作,读操作不影响
  • 读-写冲突:读前写后,回滚写
  • 写-写冲突:后来的写操作被回滚

2.3 基于有效性检查的并发控制方法

每一个事务被分为两或三个阶段,依次顺序执行:

  • 读阶段:事务Ti涉及的数据被读入Ti的局部变量中,此事务所有写操作都是对局部变量修改,因此不存在冲突,此阶段并发度很高,属于乐观技术
  • 有效性检查阶段:根据有效性检查规则,如果违反则终止当前事务
  • 写阶段:如果有效性检查通过,则把Ti被写过的局部变量值写到数据库中,只读事务不需要这个阶段

2.3.1 有效性检查规则(利用时间戳排序并发控制技术来决定可串行性)

  1. Start(Ti) 事务Ti开始执行的时间
  2. Validation(Ti) 开始有效性检查的时间
  3. Finish(Ti) Ti完成写阶段的时间
  • Va(Ti) < Va(Tj)则产生的任何调度必须等价于Ti出现在Tj之前
  • 必须保证Finish(Ti)<Start(Tj),保证可串行性,但是没有并发,所以改进:Start(Tj) < Finish(Ti) < Validation(Tj)

2.4 基于MVCC的并发控制方法

多版本并发控制技术并不是一个可独立使用的事务并发控制技术,需要基于其他并发控制技术。以下讨论多版本时间戳排序机制多版本两阶段封锁协议

2.4.1 多版本时间戳排序机制

符号说明:

  • TS(Ti):事务开始时的时间戳
  • 每个数据项X有版本序列<X1,X2...>,每个版本Xi包含三个字段
    • value,数据项的值
    • W-TS(Xi),创建这个版本的事务的时间戳
    • R-TS(Xi),所有成功读取这个版本的事务的时间戳

排序规则,保证可串行性:

  • 事务Ti执行读或写操作,找到的数据项版本Xm,其写时间戳是小于等于TS(Ti)中的最大一个(确保找到最近的版本)
  • Ti执行读操作永远不会被阻塞
  • Ti执行写操作:
    • 如果TS(Ti)<R-TS(Xm),则中止Ti。表明即将执行的写操作之后的时间上已经发生了一个读操作(读写冲突),如果允许则可能发生不可重复读异常,这是因为Ti写操作的物理时间虽然早于其他事务对Xm版本的读,但因为并发是无序的,R-TS(Xm)已经发生。
    • 如果TS(Ti)==W-TS(Xm),则更新Xm的值。表明本事务多次写过同一个数据项,新值覆盖旧值
    • 如果TS(Ti)>W-TS(Xm),则为X创建一个新版本(写写冲突)

2.4.2 多版本两阶段封锁协议

符号说明:

  • 每个数据项X有版本序列<X1,X2...>,每个版本Xi包含一个时间戳,对应唯一的事务标识(事务号)
  • 事务号是递增的数字
  • 事务分为只读事务和更新事务。事务管理器事先知道每个事务的类型,然后根据事务类型做出一定优化(多数多版本两阶段封锁协议的事务管理器对只读事务做了优化)

规则:

  • 只读事务开始时获得事务号,根据事务号读取对应的数据版本,整个生命周期内只使用这个版本的数据
  • 更新事务读操作,如果能获取该数据项的共享锁,则读取该数据项的最新版本
  • 更新事务写操作
    • 能获得该数据项的排它锁,则为该数据项创建一个版本,刚创建版本的时间戳为无穷大,避免其他并发事务读到这个尚未提交的数据项版本,事务提交时更新此版本的时间戳,表示此后其他事务可以读写此版本的数据
    • 不能获得排它锁,表明有其他事务准备或已经写了数据但没结束即锁没释放,本更新事务只能等待

2.5 基于MVCC的可串行化快照隔离并发控制方法

  • serializable snapshot isolation,简称SSI,基于MVCC中的多版本,也基于快照隔离思想
  • 为了解决快照隔离的写偏序异常问题,引入此技术
  • 整体流程与快照隔离相同,增加了一些"book-keeping"记录事务的一些信息以便动态检测是否有写偏序发生(工程实现上是检测可能发生,存在误判的可能性),如果有则回滚引发写偏序异常的事务
  • 主要内容参见论文《Serializable Isolation for Snapshot Databases》

2.5.1 理论基础

通过两篇论文《Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions》和《Making snapshot isolation serializable》定义了并发事务之间的写偏序检测方法。

解决写偏序问题就是打破环,回滚使环成立的新加入事务

2.5.2 三种依赖关系

  • 写读依赖(wr-dependency):事务T1写数据项X的一个版本,T2读这个版本,T1先于T2执行,画一条从T1到T2的实边,即对同一个版本的写操作到读操作的边
  • 写写依赖(ww-dependency):T1写数据项X的一个版本,T2使用一个新版本替换这个版本,T1需要先于T2执行完毕,画一条从T1到T2的实边,即对同一个对象的写操作到写操作的边
  • 读写依赖(rw-dependency):T1写X的一个版本,T2读这个对象之前的版本,意味着T1后于T2执行,画一条从T1到T2的虚边,即对同一个对象的写操作到读操作的边

以上学习笔记参考自《数据库事务处理的艺术:事务管理与并发控制》李海翔等著