隔离性与隔离级别

事物的隔离级别有四种,分别是:读未提交、读提交、可重复读、串行化,从前往后隔离级别越来越高,但是执行效率越来越低(涉及锁等情况)。

  • 读未提交,即事务未提交,其他事务可以查看我这个事务修改后的值。读未提交存在脏读、不可重复读、幻读问题。
  • 读提交,即事务未提交,其他事务不可以查看我当前事务修改后的值,读提交解决了读未提交中脏读问题。
  • 可重复读,即事务执行过程中,就算别的事务修改了某项数据,本事务读取该数据应该是不变的,可重复读解决了读提交中不可重复读的问题。
  • 串行化,即在事务执行全程加锁解决,串行化则解决了幻读的问题。

在MySQL中InnoDB默认隔离级别是可重复读。

MVCC

InnoDB的串行化是通过加锁实现的,而读提交和可重复读是通过MVCC(多版本并发控制)实现的。

当事务开始(可重复读)或者语句执行(读提交)时,会创建一个Read View(视图),视图的内容如下:

事物隔离及MVCC插图

  • createor_trx_id:当前事务ID,也是创建该Read Virw的事务id(按申请顺序严格递增)。
  • m_ids:事务活跃且未提交的事务id的集合。
  • min_trx_id:事务活跃且未提交的事务中最小事务id。
  • max_trx_id:已经创建过的事务最大id的下一个id。

我们把事务状态划分为几类:

事物隔离及MVCC插图1

在这里,我们可以通过createor_trx_id获取当前事务ID,低水位即min_trx_id,高水位即max_trx_id。

Read View 通过记录高水位、低水位还有活跃未提交的事务集合,储存了不同事务的执行状态。

另外,我们还需要了解InnoDB表中的隐藏列:

事物隔离及MVCC插图2

trx_id:记录事务的id

roll_pointer:指向undo日志的指针,通过undo日志回滚计算出上一个id事务的行数据。

下面简单说明MVCC实现:

1.可重复读

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

可重复读隔离级别下,当事务开始时,会创建一个Read View(视图),视图的内容在上面有提到。

当执行select语句时,事务就回根据Read View里的内容,通过undo日志一直向前查找,直到找到第一个当前事务id之前的且已提交的事务。通过这种方式,在事务启动前的更新对于本事务是可见的,事务启动后的更新对于本事务是不可见的。也就实现了可重复读。

这是怎么实现的呢?InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

我们把事务状态划分为几类:

事物隔离及MVCC插图1

在这里,我们可以通过createor_trx_id获取当前事务ID,低水位即min_trx_id,高水位即max_trx_id。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于每一个版本行的 trx_id 和这个一致性视图的对比结果得到的。

对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  • 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

  • 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

  • 如果落在黄色部分,那就包括两种情况

    1. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    2. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读(后面会说明)。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

2.读提交

读提交和可重复读类似,但是区别是创建Read View的时刻不同:

  • 可重复读是在事务启动的时候创建Read View
  • 读提交是每条select语句执行的时候,为每条select语句创建对应的Read View

判断版本可见还是不可见的规则,和可重复读一致。

这也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

当前读

如果初始k = 1,有三个事务A、B、C,他们的事务执行过程是这样的:
事物隔离及MVCC插图3

按照可重复读规则,对于事务B,当前select读到的内容是k = 1。但是我们看到,在事务B之前,事务C就对k进行了更新操作。如果事务B对更新是读到的k为1,必然会导致数据的不一致。

所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,也就是C修改后的值(最后一次修改后的值),称为“当前读”(current read)。

除了 update 语句外,select 语句如果加锁,也是当前读。比如,如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。

两阶段锁协议

还是上面的例子,如果事务C在事务B执行完更新语句后提交,那怎么办呢?

那么两阶段锁协议就排上用场了,两阶段锁协议是指在对行操作时会对该行进行加锁,当事务提交时才对该行进行解锁。

所以,如果遇到这种情况,事务B的更新语句会因为拿不到锁而被阻塞。这样就能保证事务b读到的是在该语句之前更新的最新的数据,也就能保证数据的一致了。

由于两阶段锁协议的特点,我们要尽量避免长事务。如果不可避免使用长事务,那就要将锁的影响降到最小,也就是尽量把产生互斥锁的语句放到靠后执行。

参考资料

  1. 小林Coding:消失的100万,炸了

  2. 极客时间 —— MySQL45讲 :事务隔离:为什么你改了我还看不见?

  3. 极客时间 —— MySQL45讲 :事务到底是隔离的还是不隔离的?

Categories:

Tags:

还没发表评论,快来发表第一个评论吧~

发表回复