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

然后将roll_pointer指向该
undolog,所以该列相当于一个指针,通过该列,可以找到修改之前的信息
2. MVCC详解
2.1 版本链
假设有一条记录如下:
插入该记录的
事务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日志都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条
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 概念,其包含四个核心概念:
m_ids:生成
read view时候,活跃的事物id集合
min_trx_id:
m_ids的最小值,既生成read view的时候,活跃事物的最小值
max_trx_id:表示生成
read view的时候,系统应该分配下一个事物id值
creator_trx_id:创建
read view的事物id,即当前事物id。
有了这个
ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
当记录的事物id等于creator_trx_id的时候,说明当前事物正在访问自己修改的记录,所以该版本可见 如果被访问的版本事物id小于
min_trx_id的时候,则说明,在创建
read view的时候,该事物已经提交,该版本,对当前事物可读 如果被访问的版本事物id大于或等于
max_trx_id,则说明创建该
read view的时候,该说明生成该版本记录的事物id在生成
Read view之后才开启,所以该版本不能被当前事物可读 如果被访问的版本事物
transaction_id在
m_ids集合中,说明生成
Read view的时候,该事物还是活跃的,还没有被提交,则该版本不可以被访问;如果不在,则说明创建
ReadView时生成该版本的事务已经被提交,可以被访问
注:读事物的事物id为0
在
MySQL中,
READ COMMITTED和
REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同:
READ COMMITTED—— 每次读取数据前都生成一个
ReadView
REPEATABLE READ—— 在第一次读取数据时生成一个
ReadView
下面我们通过详细例子来说明,两者有何不同:
| 时间编号 | trx 100 | trx 200 | |
|---|---|---|---|
| ① | BEGIN; | ||
| ② | BEGIN; | BEGIN; | |
| ③ | update person set grade =20 where id =1; | ||
| ④ | update person set grade =40 where id =1; | ||
| ⑤ | SELECT * FROM person WHERE id = 1; | ||
| ⑥ | COMMIT; | ||
| ⑦ | update person set grade =70 where id =1; | ||
| ⑧ | SELECT * FROM person WHERE id = 1; | ||
| ⑨ | COMMIT; | ||
| ? | COMMIT; |
在时间④中,因事务
trx 100执行了事务的提交,id=1行记录的版本链如下:
在时间⑥中,因事务
trx 200执行了事务的提交,id=1行记录的版本链如下:

在时间⑤,事务
trx 100执行
select语句时会先生成一个
ReadView,
ReadView的
m_ids列表的内容就是
[100, 200],
min_trx_id为
100,
max_trx_id为
201,
creator_trx_id为
0,此时,从版本链中选可见的记录,版本链从上到下遍历:因为grade=40,
trx_id值为
100,在
m_ids里,所以该记录不可见,同理,grade=20的也不见。继续往下遍历,grade=20,
trx_id值为
80,小于小于
ReadView中的
min_trx_id值
100,所以这个版本符合要求,返回给用户的是等级为10的记录。
在时间⑧中,如果事务的隔离级别是
READ COMMITTED,会单独又生成一个
ReadView,该
ReadView的
m_ids列表的内容就是
[200],
min_trx_id为
200,
max_trx_id为
201,
creator_trx_id为
0,此时,从版本链中选可见的记录,版本链从上到下遍历:因为grade=70,
trx_id值为
200,在
m_ids里,所以该记录不可见,继续往下遍历,grade=40,
trx_id值为
100,小于
ReadView中的
min_trx_id值
200,所以这个版本是符合要求的,返回给用户的是是等级为40的记录。
在时间⑧中,如果事务的隔离级别是
REPEATABLE READ,在时间⑧中,不会单独生成一个
ReadView,而是沿用时间5的
ReadView,所以返回给用户的等级是10。前后两次select得到的是一样的,这就是
可重复读的含义。
3. 总结
通过分析MVCC详解部分,可以得出,基于MVCC,在RR隔离级别下,很好解决了
幻读问题,但是我们知道,
select for update是产生当前读,不再是快照读,那么此种情况,MySQL又是怎么解决
幻读问题的呢?基于时间问题(整理画图的确需要花比较多的时间),此处先给结论,后面再分析在当前读的情况下,MySQL是怎么解决
幻读问题: 当前读 : 使用 Next-Key Lock(间隙锁) 进行加锁来保证不出现幻读
对于间隙锁是如何在当前读的情况下解决幻读问题的,感兴趣朋友可加个关注,点个赞
【相关推荐:mysql视频教程】
