如何实现事务的隔离性

大家都知道事务的 ACID 四大特性,其中隔离性代表事务的修改结果在什么时候能被其他事务看到。这篇文章来介绍下数据库中是如何实现事务隔离的。


一、隔离级别介绍

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non reapeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了 “隔离级别” 的概念。标准的隔离级别有:读未提交(read uncommitted)、读已提交(read commited)、可重复读(repeatable read)串行化。其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。


二、隔离的实现

隔离的实现主要有读写锁和 MVCC(Multi-Version Concurrency Control)多版本并发处理方式。


1、读写锁

最简单直接的的事务隔离实现方式,每次读操作需要获取一个共享锁,每次写操作需要获取一个写锁。共享锁之间不会产生互斥,共享锁和写锁之间、以及写锁与写锁之间会产生互斥。当产生锁竞争时,需要等待其中一个操作释放锁后,另一个操作才能获取到锁。


2、MVCC

在读写锁中,读和写的排斥作用大大降低了事务的并发效率,于是人们又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了。不同的事务 sessio n会看到自己特定版本的数据,即使其他的事务更新了数据,但是对本事务仍然不可见,本事务看到的数据始终是第一次查询到的数据。在数据库中,这个快照的处理方式叫多版本并发控制(Multi-Version Concurrency Control)。这种方式真正实现了非阻塞读,只有在写操作时才需要加行级锁,因此并发效率更高。

在各个数据库系统的,MVCC 的实现机制不尽相同,下面来详细介绍一下 InnoDB 是如何实现 MVCC 的,主要讨论可重复读级别的实现。

首先,需要了解三个概念:ReadView、undo log、可见性判断算法。


(1)ReadView

ReadView其实就是上面提到的快照,每个事务在启动后第一次执行查询时会创建一份快照,一个事务快照的创建过程可以概括为:

  1. 查看当前所有的未提交并活跃的事务,存储在数组中。
  2. 选取未提交并活跃的事务中最小的 XID,记录在快照的 xmin 中。
  3. 选取未提交事务中最大的 XID,记录快照在 xmax 中。

ReadView 主要是用来做可见性判断的,即通过 ReadView 可以知道:哪些事务的提交结果对当前事务可见,哪些事务的提交结果对当前事务不可见。关于如何判断可见性,后面部分会对可见性判断算法做出介绍。不过,我们可以先思考一个问题,在可重复读隔离级别中,哪些事务的提交结果对当前事务可见呢?


(2)undo log

刚才介绍了 ReadView 的基础概念,提到了 ReadView 是事务的快照,但是通过 ReadView 仅仅能知道哪些事务的提交结果对当前事务可见,可是还是不知道当前事务的数据快照在哪啊。undo log 就是来解决这个问题的。

undo log 就是我们通常说的回滚日志,undo log 存放的是数据的历史记录,也可以叫数据的快照。
当一个事务要提交修改时:

  1. 会用排他锁锁定该行。
  2. 将该行修改前的值 Copy 到 undo log segment(回滚段)。
  3. 修改当前行的值,将该行的回滚指针指向 undo log 中修改前的行。

下图描述了数据行和回滚段的数据关系。
如何实现事务的隔离性_第1张图片

如上图,回滚日志使用链表组织起来的,链表的每个节点都是一个数据的版本。InnoDB 在每行记录后面添加了三个字段:

  • DB_ROW_ID:包含一个随着新行插入而单调递增的行 ID,当由 innodb 自动产生聚集索引时,聚集索引会包括这个行 ID 的值,否则这个行 ID 不会出现在任何索引中。
  • DB_TRX_ID:最后一次对本行提交修改的事务 ID。同时,在回滚段中的每条记录,也包含着该条日志对应的事务 ID。
  • DB_ROLL_PTR:指向写入回滚段(rollback segment)的 undo log record(撤销日志记录记录)。回滚段的数据结构是链表,如果需要找到指定版本的数据,需要通过 DB_ROLL_PTR 指针沿着链表遍历回滚段。

(3)可见性判断算法

在介绍 ReadView 的时候,我们提出了可重复读隔离级别中事务的可见性问题。这个问题答案很简单,在可重复读隔离级别中,对于当前事务 tx_cur 来说,tx_cur 开始查询之前的已提交事务都对 tx_cur 都可见,在 tx_cur 开始查询之前的未提交事务和 tx_cur 开始查询之后的所有事务对 tx_cur 均不可见。下面用一张草图解释一下。

如何实现事务的隔离性_第2张图片

如图所示,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。

三、总结

  1. 隔离的实现主要有读写锁和MVCC(Multi-Version Concurrency Control)多版本并发处理方式。MVCC方式由于其读写不冲突的方式,相当于读写锁效率更高。
  2. undo log和ReadView通过可见性判断算法实现了基本的MVCC,从而实现了事务的隔离。

你可能感兴趣的:(MySQL学习(转载),mysql,转载,隔离级别,隔离性,事务)