mysql隔离级别和mvcc

408 阅读5分钟

四大隔离级别

读未提交:是指一个事务还没提交时,它做的变更就能被别的事务看到。
读提交:是指一个事务提交之后,它做的变更才会被其他事务看到。
可重复读:是指一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一 致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
串行化:顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突 的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。(并不是锁表,是根据where条件锁定范围,涉及到Next-key lock)

image.png

那么事务的隔离级别到底是怎么实现的呢?

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。 假设一个值从1被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录。

image.png

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看 到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
读未提交:直接返回视图上的最新值。
读提交:这个视图是在每个SQL语句开始执行的时候创建
可重复读:这个视图是在事务启动时创建的,整个事务存在期间都用这个视图
串行化:直接用加锁的方式来避免并行访问

当前值是4,对于read-viewA,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。 同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-viewA、B、C对应的 事务是不会冲突的。 你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。 也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。 什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的read-view的时候。

这也是为什么要避免长事务的原因之一 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占 用存储空间。

MVCC

InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。

上面说了mvcc的实现逻辑,通过undo log来获取不同的视图,接下来更深入的了解mvcc。

要了解mvcc,首先要知道InnoDB的表来说,每条记录都有3个隐藏列

  1. row_id: 表中没有主键或唯一索引时会生成

  2. row trx_id:每次记录发生改动,都会将事务id赋值给该列

  3. roll_pointer:回滚指针,指向undo log日志,每条undo日志也都有一个roll_pointer属性,这样可以连接成一个链表一样。

image.png

上图通过回滚指针形成了一条版本链。但其实这里视图只是一个概念性的东西并不是真实存在的(v1和v2),它们是通过undo log计算出来的。

mvcc怎么实现的呢?

InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活 跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位

那么这个事务数组就将事务分成了几种情况

image.png

  1. 比低水位小,已提交事务。可见
  2. 比高水位大,启动后创建的事务,不可见
  3. 在低水位和高水位之间,并且在数组中存在,未提交事务,不可见。
  4. 在低水位和高水位之间,并且在数组中不存在,已提交事务,可见。

上面的图一度让我对第4种情况产生了疑惑。其实黄色部分是>=未提交事务的(也就是包含关系) 比如未提交事务80,提交事务81,当前事务90。那么低水位80 高水位91,事务数组[80,90]显然81是符合第4种情况的。

image.png

事务B的update语句,如果按照一致性读,好像结果不对? 事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见2吗,怎么能 算出3来?

如果事务B在更新之前查询一次数据,这个查询返回的k的值确实是1。

但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。 因此,事务B此时的set k=k+1是在(1,2)的基础上进行的操作。 所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”。 所以,在执行事务B查询语句的时候,最新数据的版本号也是自己的版本号,可以接使用,所以查询得到的k的值是3。

  • 读提交:每条sql都获取一个新的视图

  • 读提交:事务启动时获取视图