大家都知道事务的ACID四大特性,其中隔离性代表事务的修改结果在什么时候能被其他事务看到。这篇文章来介绍下数据库中是如何实现事务隔离的。
隔离级别介绍
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non reapeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。标准的隔离级别有:读未提交(read uncommitted)、读已提交(read commited)、可重复读(repeatable read)串行化。其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
隔离的实现
隔离的实现主要有读写锁和MVCC(Multi-Version Concurrency Control)多版本并发处理方式。
1.读写锁
最简单直接的的事务隔离实现方式,每次读操作需要获取一个共享锁,每次写操作需要获取一个写锁。共享锁之间不会产生互斥,共享锁和写锁之间、以及写锁与写锁之间会产生互斥。当产生锁竞争时,需要等待其中一个操作释放锁后,另一个操作才能获取到锁。
2. MVCC
在读写锁中,读和写的排斥作用大大降低了事务的并发效率,于是人们又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了。不同的事务session会看到自己特定版本的数据,即使其他的事务更新了数据,但是对本事务仍然不可见,本事务看到的数据始终是第一次查询到的数据。在数据库中,这个快照的处理方式叫多版本并发控制(Multi-Version Concurrency Control)。这种方式真正实现了非阻塞读,只有在写操作时才需要加行级锁,因此并发效率更高。
在各个数据库系统的,MVCC的实现机制不尽相同,下面来详细介绍一下InnoDB是如何实现MVCC的,主要讨论可重复读级别的实现。
首先,需要了解两个概念:ReadView、undo log、可见性判断算法
ReadView
ReadView其实就是上面提到的快照,每个事务在启动后第一次执行查询时会创建一份快照,一个事务快照的创建过程可以概括为:
- 查看当前所有的未提交并活跃的事务,存储在数组中
- 选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
- 选取未提交事务中最大的XID,记录快照在xmax中
ReadView主要是用来做可见性判断的,即通过ReadView可以知道:哪些事务的提交结果对当前事务可见,哪些事务的提交结果对当前事务不可见。关于如何判断可见性,后面部分会对可见性判断算法做出介绍。不过,我们可以先思考一个问题,在可重复读隔离级别中,哪些事务的提交结果对当前事务可见呢?
undo log
刚才介绍了ReadView的基础概念,提到了ReadView是事务的快照,但是通过ReadView仅仅能知道哪些事务的提交结果对当前事务可见,可是还是不知道当前事务的数据快照在哪啊。undo log就是来解决这个问题的。
undo log就是我们通常说的回滚日志,undo log存放的是数据的历史记录,也可以叫数据的快照。
当一个事务要提交修改时:
1.会用排他锁锁定该行
2.将该行修改前的值Copy到undo log segment(回滚段)。
3.修改当前行的值,将该行的回滚指针指向undo log中修改前的行。
下图描述了数据行和回滚段的数据关系。
如上图,回滚日志使用链表组织起来的,链表的每个节点都是一个数据的版本。InnoDB在每行记录后面添加了三个字段:
DB_ROW_ID: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
DB_TRX_ID: 最后一次对本行提交修改的事务ID。同时,在回滚段中的每条记录,也包含着该条日志对应的事务ID。
DB_ROLL_PTR:指向写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。回滚段的数据结构是链表,如果需要找到指定版本的数据,需要通过DB_ROLL_PTR指针沿着链表遍历回滚段。
可见性判断算法
在介绍ReadView的时候,我们提出了可重复读隔离级别中事务的可见性问题。这个问题答案很简单,在可重复读隔离级别中,对于当前事务tx_cur来说,tx_cur开始查询之前的已提交事务都对tx_cur都可见,在tx_cur开始查询之前的未提交事务和tx_cur开始查询之后的所有事务对tx_cur均不可见。下面用一张草图解释一下。
如图所示,tx1-tx6分别是按时间顺序的6个数据库事务,假设当前启动的事务是tx_cur,其中tx1-tx2是tx_cur开始查询之前的已提交事务,tx3-tx5是tx_cur开始查询时正在进行的活跃事务,tx6是开始查询之后的提交的事务。
在tx_cur启动事务并开始第一次查询时,会创建一个ReadView,ReadView中存储的是当前正在活跃的所有未提交事务id。在ReadView之前的已提交事务对tx_cur可见,在ReadView中以及ReadView之后的事务对tx_cur均不可见。
上面是一个简单的事务可见性判断过程,那么当前事务该如何找到正确版本的数据呢?这个需要结合undo log一起来说。
我们可以结合undo log那节的示意图来看。
1.首先查询行的DB_TRX_ID字段,该字段记录的是当前行最后提交的事务ID,简称为tx_id。
2.通过ReadView判断tx_id是否对tx_cur可见。若可见,即找到了正确版本的数据;若不可见,则通过DB_ROLL_PTR指针找到undo log的上一个版本记录,重复过程1。
总结
- 隔离的实现主要有读写锁和MVCC(Multi-Version Concurrency Control)多版本并发处理方式。MVCC方式由于其读写不冲突的方式,相当于读写锁效率更高。
- undo log和ReadView通过可见性判断算法实现了基本的MVCC,从而实现了事务的隔离。