mysql幻读及MVCC实例解析
个人笔记复制过来的 懒得排版了,这里格式会好点
https://www.wolai.com/jtaGKJqoUusS5mmA5NqoG1
参考文章
网上有太多文章不能自圆其说了,也没时间看源码,只能基于他们的信息推断出一个能自圆其说的推断,欢迎指正,下面的变量命名以5.6为准,8.0的命名有些许修改,但是总体逻辑没改变
有基础的同学直接看下面的例子,看看实际结果是否符合预期,如果不符合再看分析会比较容易理解
其实个人觉得研究这种东西挺浪费时间的,学习深层次思想才是该做的东西
官方定义:
The so-called
phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom”row.
翻译成人话就是
两次查询,第二次查询的条数比第一次查询的多,这就是幻读
事务级别(SQL标准):
隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ UNCOMMITTED | Possible | Possible | Possible |
READ COMMITTED | Not Possible | Possible | Possible |
REPEATABLE READ | Not Possible | Not Possible | Possible |
SERIALIZABLE | Not Possible | Not Possible | Not Possible |
MVCC
MySQL在REPEATABLE READ隔离级别下,是可以通过MVCC禁止幻读问题的发生的
但是也会存在特殊场景
快照读(无幻读)
在正常快照读下是可以禁止幻读发生
当前读(无幻读)
因为间隙锁的存在,所以也没有幻读发生
当前读+本事务更新(发生幻读)
其中一条记录被称为undo日志,这个链表被称为版本链
在此基础上,mysql有一个ReadView的概念
- trx_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
这里也有个异议点,部分文章说的是trx_ids是不包含当前事务的id,但是包不包含其实都不影响判断,下面文章以包含来算
- up_limit_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是trx_ids中的最小值。 如果trx_ids为空,则为low_limit_id
- low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
- creator_trx_id:表示生成该ReadView的事务的事务id。
异议点来了,这里的creator_trx_id应该只能为指向trx_t结构体的trx_id的一个指针(或者也有可能直接调用的trx_get_id_for_print
方法) 再简单点来说,这个值在发生读写事务切换时是会发生改变的,因为对于mysql5.7来说,开启事务(BEGIN
,START TRANSACTION
,非START TRANSACTION WITH CONSISTENT SNAPSHOT
)的时候事务id为0,而这时候想当于有一个trx_t的结构体,只不过里面的trx_id
为0,如果这时该事务变成了只读事务(执行INSERT、DELETE、UPDATE这些语句时),那么会为事务分配真实事务id,折时候的trx_id
会变成真实值. 而通过SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
语句输出时,实际上是调用trx_get_id_for_print
方法,当trx_id=0时,输出的是根据指针算出来的值
判断逻辑:
-
db_trx_id
<up_limit_id
||db_trx_id
==creator_trx_id
(显示)如果数据事务ID小于read view中的
最小活跃事务ID
,则可以肯定该数据是在当前事务启之前
就已经存在
了的,所以可以显示
。或者数据的
事务ID
等于creator_trx_id
,那么说明这个数据就是当前事务自己生成的
,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示
的。 -
db_trx_id
>=low_limit_id
(不显示)如果数据事务ID大于read view 中的当前系统的
最大事务ID
,则说明该数据是在当前read view 创建之后才产生
的,所以数据不显示
。如果小于则进入下一个判断 -
db_trx_id
是否在活跃事务
(m_ids
)中不存在
:则说明read view产生的时候事务已经commit
了,这种情况数据则可以显示
。已存在
:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。
rr和rc的区别就是生成ReadView的时机不同,rr只有再第一次查询时生成ReadView,而rc在每一次查询时都会生成ReadView
ReadView和记录改动是两回事,ReadView只会被select
/START TRANSACTION WITH CONSISTENT SNAPSHOT
触发生成
场景举例
CREATE TABLE test
(
id
int(11) DEFAULT NULL,
name
varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
RR级别
db_trx_id
< up_limit_id
session A | session B |
trx_id=1 | trx_id= |
begin; |
|
insert into test(id,name) values(1,'1'); <br>db_trx_id=1 |
|
commit ; |
|
begin; |
|
Select * from test; <br>trx_ids:[]<br>up_limit_id:2<br>low_limit_id:2<br>creator_trx_id:0<br><br>db_trx_id=1:<br>db_trx_id<low_limit_id ,数据可见 |
db_trx_id
= creator_trx_id
||db_trx_id=low_limit_id
session A | session B |
trx_id=1 | trx_id=2 |
begin; |
|
begin; |
|
Select * from test;<br> trx_ids:[]<br>up_limit_id:1<br>low_limit_id:1<br>creator_trx_id:0<br><br><br> |
|
insert into test(id,name) values(1,'1'); <br>db_trx_id=1 |
|
commit ; |
|
Select * from test;<br> trx_ids:[]<br>up_limit_id:1<br>low_limit_id:1<br>creator_trx_id:0<br>db_trx_id=low_limit_id 数据不可见 |
|
update test set name = '2' where id = 1 <br>分配trx_id为2<br>db_trx_id=2 |
|
Select * from test; <br>trx_ids:[]<br>up_limit_id:1<br>low_limit_id:1<br>creator_trx_id:2<br><br>db_trx_id=2<br>db_trx_id = creator_trx_id 数据可见 |
db_trx_id
≥ low_limit_id
session A | session B | session c |
trx_id=1 | trx_id=0 | trx_id=2 |
begin; |
||
begin; |
||
Select * from test;<br> trx_ids:[]<br>up_limit_id:1<br>low_limit_id:1<br>creator_trx_id:0<br><br><br> |
||
insert into test(id,name) values(1,'1'); <br>db_trx_id=1 |
||
commit ; |
||
begin; |
||
insert into test(id,name) values(2,'2'); <br>db_trx_id=2 |
||
commit ; |
||
Select * from test;<br> trx_ids:[]<br>up_limit_id:1<br>low_limit_id:1<br>creator_trx_id:0<br><br>db_trx_id=1:<br>db_trx_id=low_limit_id 数据不可见<br><br>db_trx_id=2:<br>db_trx_id>low_limit_id 数据不可见 |
up_limit_id
≤db_trx_id
< low_limit_id
session A | session B<br> | session C |
trx_id=1 | trx_id=2 | trx_id=0 |
begin; |
||
Begin; |
||
insert into test(id,name) values(1,'1'); <br>db_trx_id=1 |
||
insert into test(id,name) values(2,'2'); <br>db_trx_id=2 |
||
commit; |
||
Select * from test; <br>trx_ids:[1]<br>up_limit_id:1<br>low_limit_id:3<br>creator_trx_id:0<br><br>db_trx_id=1:<br>low_limit_id >db_trx_id ≥up_limit_id &&db_trx_id in trx_ids <br>所以数据不可见<br>db_trx_id=2:<br>low_limit_id>db_trx_id ≥up_limit_id &&db_trx_id not in trx_ids <br>所以数据可见<br><br> |