MySQL是如何解决幻读问题的?

金不三,银不四的高频面试题中,MySQL的事务特性,隔离级别等问题也是非常经典八股文之一,面对此种问题,估计绝大数小伙伴也是信手拈来的事情:
事物特性(ACID):原子性(Atomicity)、隔离性(Isolation)、一致性(Consistency)和持久性
隔离级别:读取未提交(READ UNCOMMITTED),读取已提交(READ COMMITTED),可重复读(REPEATABLE READ),可串行化(SERIALIZABLE)
而每一种隔离级别导致的问题有:

  • READ UNCOMMITTED隔离级别下,可能发生脏读、不可重复读和幻读问题
  • READ COMMITTED隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题
  • REPEATABLE READ隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题
  • SERIALIZABLE隔离级别下,各种问题都不可能发生
对于MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读),从上面的SQL标准的四种隔离级别定义可知,REPEATABLE-READ(可重复读)是不可以防止幻读的,但是我们都知道,MySQL InnoDB存储引擎是解决了幻读问题发生的,那他又是如何解决的呢?
1. 行格式  在进入主题之前,我们先大致了解一下什么是行格式,这样有助于我们理解下面的MVCC,行格式是表中的行记录在磁盘的存放方式,Innodb存储引擎总共有4种不同类型的行格式:compact、redundant、dynamic、compress;虽然很很多行格式,但是在原理上,大体都相同,如下,为compact行格式:
MySQL是如何解决幻读问题的?

文章插图
 
  从图中可以看出来,一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分,记录的额外信息分别是变长字段长度列表、NULL值列表和记录头信息,而记录的真实数据除了我们自己定义的列之外,MySQL会为每个记录添加一些默认列,这些默认列又称为隐藏列,具体列如下:
列名
长度
描述
row_id
6个字节
行ID,唯一标识一条记录
transaction_id
6个字节
【MySQL是如何解决幻读问题的?】事务ID
roll_pointer
7个字节
回滚指针
隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的,画得再详细一点,compact行格式如下:
MySQL是如何解决幻读问题的?

文章插图
 
  • transaction_id :事物id,当事物对行记录进行修改时,都会将本事物的事物id赋值到该列
  • roll_pointer:每次在对行记录进行改动的时候,都会把旧版本的数据写入undolog日志,然后将roll_pointer 指向该undolog,所以该列相当于一个指针,通过该列,可以找到修改之前的信息
2. MVCC详解2.1 版本链假设有一条记录如下:
MySQL是如何解决幻读问题的?

文章插图
 
插入该记录的事务id为80,roll_pointer 指针为NULL(为了便于理解,读者可理解为指向为NULL,实际上roll_pointer第一个比特位就标记着它指向的undo日志的类型,如果该比特位的值为1时,就代表着它指向的undo日志类型为insert undo)
假设之后两个事务id分别为100、200的事务对这条记录进行UPDATE操作:
-- 事务id=100 update person set grade =20 where id =1; update person set grade =40 where id =1; -- 事务id=200 update person set grade =70 where id =1;  每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
MySQL是如何解决幻读问题的?

文章插图
 
  对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值 。另外,每个版本中还包含生成该版本时对应的事务id
2.2 ReadView  对于数据库的四种隔离级别:1)read uncommitted;2) read committed;3) REPEATABLE READ; 4)SERIALIZABLE;来说,READ UNCOMMITTED,每次读取版本链的最新数据即可;SERIALIZABLE,主要是通过加锁控制;而read committed和REPEATABLE READ都是读取已经提交了的事物,所以对于这两个隔离级别,核心问题是版本链中,哪些事物是对当前事物可见;为了解决这个问题,MySQL提出了read view 概念,其包含四个核心概念:


推荐阅读