一次性掌握innodb引擎如何解决幻读和不可重复读
了解mysql的都知道,在mysql的RR(可重复)隔离级别下解决了幻读和不可重复。你知道RR下是怎么解决的吗,很多人会回答是通过MVCC和next-key解决的,具体是怎么解决的,今天来重点分析下。
mysql的隔离级别都不陌生了,简单回顾下四种隔离级别:RU(读未提交)、RC(读已提交)、RR(可重复读)【默认隔离级别】、Serializable(可串行化)。四种隔离级别控制力度有浅到深。日常的系统中常使用RC、RR两种,最常用的是RR。
一、认识两种读
说起mysql,很多人会给你说有两种读:快照读和当前读。我更喜欢在他们前面加个修饰那就是在RC、RR隔离级别下才有快照读和当前读。为什么这样说,这是因为只有在这两种隔离级别下才会有两种读。其他则是只有一种读那就是当前读。
在RU隔离级别下,不需要进行控制,可以读到其他事务未提交的事务,那肯定是当前读了。
在Serializable隔离级别下,所有的sql都会加锁,查询类型的sql加S(共享)锁、更新类型的sql加X(排它)锁。自然也都是当前读。
在RC、RR隔离级别下分别要解决脏读、幻读和不可重复读,所以才有了快照读和当前读。
关于对快照读和当前读的理解,从字面意思上理解即可,快照读也就是会读取一个快照版本,或者说历史版本;当前读就是读取当前的最新的数据。
在RU、Serializable隔离级别下,普通的select语句都是当前读;在RC、RR隔离级别下普通的select语句都是快照读。由于平时使用RR隔离级别多,所以,默认select语句都是快照读。
不论在哪种隔离级别下更新操作(insert/update/delete)都是当前读,当前读是要加锁的;下面的sql也是当前读,
select ... for update; 加X(排它)锁
select ... in share mode; 加S(共享)锁
二、认识MVCC
MVCC(Multi-Version Concurrency Control)翻译过来是多版本并发控制,存在于RC、RR隔离级别下,用于快照读。是这样来实现的,存储引擎全局维护了一个系统版本号,每开启一个新的事务,这个系统版本号就会递增。事务开始时刻的系统版本号,会作为这个事务本身的版本号。在每行记录中,存储引擎又在每行的后面保存两个隐藏的列,分别保存这一行的开始版本号(trx_id)和过期版本号(roll_pointer)。版本号就是事务ID。
看下各种更新语句版本号的情况,
insert,存储引擎为新插入的每一行保存当前的系统版本号作为这一行的开始版本号。
update,存储引擎会新插入一行记录,当前的系统版本号是新记录行的开始版本号;同时会将当前行的过期版本号设为原来行的系统版本号;
delete,存储引擎将删除的记录行的过期版本号设置为当前的系统版本号。
还要了解另外一个概念,readview。
readview用来判断版本链中哪个版本对当前事务是可见的,包含4个重要属性:
m_ids:生成ReadView时,当前系统活跃的事务id列表;
min_trx_id:列表中的最小事务id;
max_trx_id:列表中最大事务id;
creator_trx_id:生成该ReadView自身事务的id。
注意:如果一个事务只读,则creator_trx_id默认为0,只有当事务发生INSERT和UPDATE、DELETE操作时才会分配该事务id。creator_trx_id是为了判断这条undo log是否是自己生成的。
如何判断数据的可见性,
- 从记录的最新版本开始迭代依次取隐藏列trx_id和ReadView的m_ids、min_trx_id、max_trx_id比较。
- 如果trx_id等于creator_trx_id表示该版本是自己更新的,版本可见;
- 如果trx_id在m_ids列表中,则该版本不符合可见性要求;
- 如果版本trx_id小于min_trx_id,表示事务在创建ReadView时已经提交,版本可见;
- 如果版本trx_id大于max_trx_id表示,该事务是在ReadView生成之后创建和提交的,不符合可见性要求;
简单点来说,MVCC就是基于记录的事务id做控制,一条记录会有多个事务id,代表多个事务id的记录会存储在undo log中。
三、解决幻读和不可重复读
3.1、select
在RC隔离级别下,解决了脏读的情况,是怎么解决的呐,当然是通过MVCC,因为MVCC是基于事务ID,也就是trx_id,所以在select的时候读取的一定是commit之后的数据,不提交没有trx_id哦,注意在RC隔离级别下是每次select都会产生一个独立的readview,所以幻读和不可重复读是无法解决的。
在RR隔离级别下,肯定脏读是不存在的。同时解决了幻读和不可重复读,也是通过MVCC,只不过这里生成readview的时机和RC隔离级别下不一样,在RR隔离级别下readview是在事务的第一个select的时候生成的,以后的select会使用第一个select的readview,这样就可以解决了幻读和不可重复读。
3.2、update/insert/delete
update/insert/delete语句都是当前读,为什么在更新语句中会有读,这是因为在执行更新操作的时候肯定要先读出来,再进行更新,这里的读便是当前读,只不过这里的当前读在RR隔离级别下是通过加next-key解决的,所以在RR隔离级别下可以解决幻读和不可重复读。
总结
本文重点分享了mysql的innodb引擎如何解决幻读和不可重复读。重点关注两点,
1、MVCC,快照读的时候使用MVCC;
2、next-key,当前读加next-key;
推荐阅读
花了一周时间,总算把mysql的加锁搞清楚了,再也不怕间隙锁和next-key了