深入理解mysql中的MVCC
一. 概念
先简单介绍一些概念,如果你已经知道了可以快速跳过该部分内容
MVCC:Multi-Version Concurency Control,即多版本并发控制,它能够更好的帮助数据组提升并发读写能力
隔离级别:mysql提供四种隔离级别,分别是READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ,SERIALIZABLE,默认的是REPEATABLE READ
mysql不同的隔离级别可能遇到不一样的问题,如,脏读,不可重复读,缓读,
脏读:如果一个事务读到了另外一个未提交事务的修改过的数据,那就意味着发生了脏读。
不可重复读:如果一个事务只能读到另外一个已经提交的事务的修改过得数据,并且其他事务每次对该数据进行一次修改并提交后,该事务都能查询到最新值,那就意味着发生了不可重复读。
缓读:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,则原先的事务再次按照该条件查询时,能把另外一个事务插入的记录也读取出来,那就意味着发生了缓读。
下面简单用一张表来表示不同的事务隔离级别可能带来的哪些问题
二. MVCC实现原理
学习MVCC一定要熟悉它的原理,不然光靠脑瓜子记忆,我相信很多人是很容易忘记,或者很容易搞混淆,下面我们就来一起学习一下它的实现原理。
1. 版本链
mysql为什么能够提升并发的读写能力就是依赖于这个版本链。就是mysql对每一条记录都会维护多个版本,就好比我们写毕业论文的时候自己会命名如下的多个版本一样(“毕业论文初版”,“毕业论文终极版1”,“毕业论文终极版2”)是一样的道理。但是mysql会将这些版本按照一定的顺序串连起来形成一个链表,这里就要借助于mysql的undo日志和roll_pointer
1.1. undo
这里先简单介绍一下undo日志,有时间我再专门一遍关于mysql的内存布局相关的内容,大家先简单理解这个undo日志里面就是存放的旧版本的记录
1.2. roll_pointer
顾名思义这里叫回滚指针,我们mysql每一行的数据除了有我们自己的写入的行数据一外还保存了下面三个隐藏列
roll_pointer:用于指向上一个版本undo日志,可以理解为一个指针
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的id赋值给trx_id隐藏列
row_id:当你设计的表没有主键,同时也没有唯一索引是,则mysql会自动为你创建这个字段,用于做聚簇索引
下面举一个例子来
表结构如下:
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', `age` int(1) NOT NULL DEFAULT '18', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8
数据如下:
trx80 | trx100 |
---|---|
begin | |
begin | |
update test set name="taolong" where id =2; | |
update test set name="longlong" where id =2; | |
commit; | |
update test set name="taotao" where id =2; | |
update test set name="tt" where id =2; | |
commit; | |
那么此时生成的版本链如下:
上面就形成了一个版本链,MVCC的并发控制就是基于一定的规则在不同的版本上进行操作和查询
2. 规则
上面已经介绍了版本链了,下面就介绍一下基于什么规则来执行MVCC并发读写的。
2.1. READ UNCOMMITTED
对于READ UNCOMMITTED隔离级别来说,直接读取最新的版本就行
2.2. SERIALIZABLE
对于SERIALIZABLE隔离级别来说,InnoDB使用枷锁的方式来访问记录
上面两种方式基本不会使用,就不介绍了
2.3. ReadView
重点需要理解的是 READ COMMITTED和REPEATABLE READ,要理解他们的差异以及原理,首先需要理解mysql提出的一个ReadView的概念。Read View包含如下4个比较重要的内容:
-
m_ids:表示在生成ReadView时,当前系统中活跃的读写事务的事务id列表
-
min_trx_id:简单来说就是m_ids中的最小值
-
max_trx_id:简单来说就是m_ids中最大的值+1
-
creator_trx_id:表示生成该ReadView的事务的事务id
有了上述ReadView, READ COMMITTED和REPEATABLE READ在访问记录时按照下面的步骤判断某个版本是否可见
-
如果被访问的版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过得记录,所以该版本可以被当前事务访问
-
如果被访问的版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
-
如果被访问的版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本被当前事务访问
-
如果被访问的版本的trx_id属性值在ReadView的min_trx和max_trx_id之间,那么需要判断判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
-
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,以此类推,知道版本链中最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含记录。
在mysql中,READ COMMITTED和REPEATABLE READ 隔离级别的一个非常大的区别就是它们生成ReadView时机不同
READ COMMITTED:每次读取数据前都生成一个ReadView
REPEATABLE READ:只在第一次读取数据时生成一个ReadView
3. 验证
3.1.1 验证READ COMMITTED
首先设置隔离级别为READ COMMITTED
set global transaction isolation level read committed;#(设置全局级别) set session transaction isolation level read committed;#(设置会话级别) select @@global.tx_isolation,@@tx_isolation;#(查询)
trx80 | trx100 |
---|---|
begin | |
begin | |
update test set name="taolong" where id =2; | |
select *from where id = 2;#(1) | |
select * from test where id =2;#(2) | |
commit; | |
select * from test where id =2;#(3) | |
commit; | |
上述执行结果如下:
#(1)执行结果如下:
解析:满足上述条件1,creator_trx与trx_id相同,查询自己的事务的版本,可见
#(2)执行结果如下:
当前事务是READ COMMITTED
解析:事务80没有提交,所以在事务100执行#(2)生成的ReadView中 80是在m_idx中,是属于当前活跃的事务,所以是不可见的
#(3)执行结果如下:
当前事务是READ COMMITTED
解析:事务80已经提交,所以在事务100执行#(3)重新生成的ReadView中 80是不在m_idx中,所以是可见的
3.1.2 验证REPEATABLE READ
首先设置隔离级别为READ COMMITTED
set global transaction isolation level repeatable read;#(设置全局级别) set session transaction isolation level repeatable read;#(设置会话级别) select @@global.tx_isolation,@@tx_isolation;#(查询)
我们还用上述的例子还做实验
trx80 | trx100 |
---|---|
begin | |
begin | |
update test set name="longlong" where id =2; | |
select *from where id = 2;#(1) | |
select * from test where id =2;#(2) | |
commit; | |
select * from test where id =2;#(3) | |
commit; | |
执行结果如下:
解析
#(1)和#(2)解析同上面一样
#(3)因为此时的隔离级别为REPEATABLE READ所以在执行#(3)时没有生成新的ReadView,还是用了第一次生成ReadView,所以其结果与#(2)执行的结果是一样的。
所以对这上面的流程分析结果,是不是so easy!!!!,接下来我要放大招了,REPEATABLE READ隔离级别其实不能够完全解决缓读问题,哈哈....你们此时会不会认为我是瞎说的,绝大多数的课本以及书上都是说mysql 的REPEATABLE READ可以解决缓读,为什么你说不能解决缓读,难道你要挑战权威吗?
不急让我们慢慢来通过例子演示
trx80 | trx100 |
---|---|
begin | |
begin | |
insert into test (id,name,age) values (3,"ttt",14); | |
select *from where id = 3; | |
select * from test where id =3; | |
commit; | |
select * from test where id =3; | |
update test set name="honghong" where id=3; | |
select * from test where id =3; | |
commit; |
结果如下图所示:
为什么会出现这个现象,大家可以按照上述的规则进行自行分析,我相信大家都能分析出来的