前言
我是幻读,据说有人认为我是MVCC解决的,为了让你们更全面地理解我,只能亲自来解释一下。算法
系列文章
1. 揭开MySQL索引神秘面纱
2. MySQL查询优化必备
3. 上来就问MySQL事务,瑟瑟发抖...
4. MVCC:据说有人好奇个人底层实现
session
1、我是谁?
先给你们作一个简单的自我介绍,我就是事务并发时会产生的三大问题之一。数据结构
个人其它俩兄弟脏读、不可重复读被MVCC在上一个回合无情的干掉了,至于上个回合发生了什么能够去看剧情回顾。并发
个人由来就是由于主人在操做一组数据时还有不少人也在对这组数据进行操做。学习
举一个简单的案例:测试
根据条件在对一组数据进行过滤返回的结果为100个,可是在主人操做的同时其余人又新增了符合条件的数据,而后主人再次进行查询时返回结果为101。第二次返回的数据跟第一次返回数据不一致。优化
因而我诞生了,你们还给我起了个很好听的名字幻读
。url
为何会给我起这个名字呢!那是由于我给人们的现象好像出了幻觉同样。.net
2、为何有人会认为我是被MVCC干掉的
为了演示方便,就直接使用以前的测试表来进行操做。线程
同时你们能够看到此表还有一些测试数据,一切从头开始,清空表。
清空表的命令truncate table_name
执行这个命令会使表的数据清空,而且自增ID会从1开始。
从执行过程来看,truncate table相似于drop table而后在create table,这里的环境都是测试环境,千万不要在线上进行操做,由于它绕过了DML方法,是不能回滚的。
进行了一点小插曲,进入正题。
根据上图的执行步骤,预期来讲左边事务的第一条select语句查询结果为空。
第二个select查询结果为1条数据,包含右边事务提交的数据。
但在实际测试的状况下,第一次执行select和第二次执行select返回结果一致。
从这个案例中,能够得出结论确实在不可重复隔离级别下会解决幻读问题(在快照读的前提下)。
3、我真的是被MVCC解决的?
经过上述测试案例来看,貌似在MySQL中经过MVCC就解决个人引来的问题,那既然都解决了个人问题,为何还有串行化的隔离级别呢!好疑惑啊!
带着这个疑问继续进行实验,为了方便就再也不使用上边表结构了,创建一个简单的表结构。
再进入一个小插曲你知道在MySQL终端如何清屏吗?
执行命令system clear便可
接着开始新一轮的测试
上图案例事务1几回查询数据都是空。
此时事务2已经成功将数据插入而且提交。
但当事务1几回查询数据为空以后进行数据插入时,提示主键重复。
再来看一个案例
- step1:事务1开启事务
- step2:事务2开启事务
- step3:事务1查询数据只有一条数据
- step4:事务2添加一条数据
- step5:事务1查询数据为一条
- step6:事务2提交事务
- step7:事务1查询数据为一条
- step8:事务1修改name
- step9:猜测一下此时表内数据会发生什么改变
此案例中事务1始终读取数据都是一条数据,可是在修改数据时影响数据行数倒是2,再次进行查看数据时居然出现了事务2添加的数据。这也能够看做是一种幻读。
小结
经过以上俩个案例得知在MySQL可重复读隔离级别中并无彻底解决幻读问题,而只是解决了快照读下的幻读问题
。
而对于当前读的操做依然存在幻读问题
,也就是说MVCC对于幻读的解决是不完全的。
4、再聊当前读、快照读
在上一回合中快照读、当前读已经被消化了,为了防止消化不良这里再简单说明一下。
当前读
全部操做都加了锁,而且锁之间除了共享锁都是互斥的,若是想要增、删、改、查时都须要等待锁释放才能够,因此读取的数据都是最新的记录。
简单来讲,当前读就是加了锁的,增、删、改、查,无论锁是共享锁、排它锁均为当前读。
在MySQL的Innodb存储引擎下,增、删、改操做都会默认加上锁,因此增、删、改操做默认就为当前读。
快照读
快照读的出现旨在提升事务并发性,实现基于个人敌人MVCC
简单来讲快照读就是不加锁的非阻塞读,即简单的select操做(select * from user)
在Innodb存储引擎下执行简单的select操做时,会记录下当前的快照读数据,以后的select会沿用第一次快照读的数据,即便有其它事务提交也不会影响当前的select结果,这就解决了不可重复读问题。
快照读读取的数据虽然是一致的,但有可能不是最新的数据而是历史数据。
5、告诉大家吧!当前读的状况下我是被next-key locks干掉的
第二小节中得知在快照读下因为我引起的问题已经被MVCC消灭了。
可是在小节三进行案例测试发如今当前读下我又满血复活了。
我要是那么容易被干掉还怎么被称为打不死的小强,这不是闹笑话呢!
说归说,闹归闹若是MVCC把它的小弟next-key locks带上那我就完了,就再也不像灰太狼说经典语录“我必定会回来的”
此时就要思考一个问题,在Innodb存储引擎下,是默认给快照读加next-key locks,仍是说须要手动加锁。
经过官方文档对于next-key locks
的解释。
To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
大体意思,为了防止幻读,Innodb使用next-key lock算法,将行锁(record lock)和间隙锁(gap lock)结合在一块儿。Innodb行锁在搜索或者扫描表索引时,会在遇到的索引记录上设置共享锁或者排它锁,所以行锁实际是索引记录锁。另外, 在索引记录上设置的锁一样会影响索引记录以前的“间隙(gap)”。即next-key lock是索引记录行加上索引记录以前的“gap”上的间隙锁定。
而且还给了一个案例SELECT * FROM child WHERE id > 100 FOR UPDATE;
当Innodb扫描索引时,会将id大于100地上锁,阻止任何大于100的数据添加。
到这里就回答了上边问题,在Innodb下解决当前读产生的幻读问题须要手动加锁来解决。
再来看一个案例
下图为此时的数据状况
下图的这个案例就解决了在第三节中第一个案例的幻读问题。
- step事务1:开启事务
- step事务2:开启事务
- step事务1:查询ID为4的这条数据而且加上排它锁
- step事务2:添加ID为4的数据,而且等待事务1释放锁
- step事务1:添加ID为4的数据,添加成功
- step事务1:查询当前数据
- step事务1:提交事务
- step事务2:报错,返回主键重复问题。
这个案例查询的索引列是主键而且是惟一的,此时Innodb引擎会对next-key lock作降级处理,也就是只锁定当前查询的索引记录行,而不是范围锁定。
案例二
仍是使用上边的数据,可是此次咱们进行一次范围查找。
此时的数据为1,3,5,查找的范围为大于3。
从下图能够看出当事务2执行添加ID为2的是能够添加成功的。
可是当添加 ID 6时须要等待。
此时若事务1不提交事务,事务2添加ID为6的这条数据就执行不成功。
对于上述的SQL语句select * from user where id > 3 for update;
执行返回的只有5这一行数据。
此时锁定的范围为(3,5],(5,∞),因此说id为2的能够插入,ID为4或者大于5的都是插入不了的。
以上就是在Innodb中解决幻读问题最终方案。
6、幻读解决方案
为了方便你们直观了解幻读的解决方案,这里咔咔进行简单的总结。
经过MVCC解决了快照读下的幻读问题,为何能解决?在第一次执行简单的select语句就生成了一个快照,而且在后边的select查询都是沿用第一次快照读的结果。因此说快照读查询到的数据有多是历史数据。
经过next-key lock解决当前读的幻读问题,next-key lock是record lock和gap lock的结合,锁定的是一个范围,若是查询数据为索引记录行,则只会锁定当前行,也就是说降级为record lock。若为范围查找时就会锁定一个范围,例如上例中ID为1,3,5查询大于3的数据,则会把(3,5],(5,∞)进行范围锁定,其它事务在锁未释放以前是没法插入的。
从官方文档还可得知若是须要验证数据惟一性只须要给查询加上共享锁便可,也就是给select 语句加上 in lock share mode,若是返回结果为空,则能够进行插入,而且插入的这个值确定是惟一的。一样也能够添加next key lock防止其余人同时插入相同数据,小节5的全部案例就是使用的next-key lock,从这一点能够得知next-key lock是能够锁定表内不存在的索引。
根据上述结论来看,若是想要检测数据惟一性使用共享锁,那么多个事务同时开启共享锁,又同时添加相同的数据怎么办,会不会出现问题呢?明确地说明是不会的,若是多个事务同时插入相同数据只会有一个事务添加成功,其它事务会抛出错误,这个就是一个新的概念“死锁”。
7、扩展
事务ID是在什么时候分配的?
在本文或者其它资料中都能获得一个信息就是当执行一条简单的select语句同时也会生成read-view。
虽然快照读、read-view都是基于事务启动的前提下,可是read-veiw是经过未提交事务ID组成的。
那么究竟是在什么时候分配事务ID的呢?
事务的启动方式有两种,分别为显示启动、另外一种是设置autocommit=0后执行select就会启动事务。
在显示启动中最简单的就是以begin语句开始,也可使用start transaction开启事务。
若使用start trancaction开启事务也能够选择开始只读事务仍是读写事务。
看了不少资料都说当开启一个事务时会分配一个事务ID,那么来验证一下是这个样子的吗?
经过上图能够看到当执行一个begin语句以后查询事务ID是空的,也就说当执行begin后并无分配trx_id。
那么当执行begin后在支持DML语句呢!
根据文档得知
执行begin命令并非真正开启一个事务,仅仅是为当前线程设定标记,表示为显式开启的事务。
因此要明白对数据进行了增、删、改、查等操做后才算真正开启了一个事务,此时会去引擎层开启事务。
为何事务ID差别特别大?
上图中查询了当前活跃的事务ID,可是两个事务ID的差别特别大。
相信不少小伙伴都遇到过这个问题,有问题不惧怕,惧怕的是没有问题。
事实上在这两条数据中只有20841是真正的事务ID,那么第二条数据中的ID是什么呢!
想知道这个数字是什么的前提是知道是怎么来的。
从上图能够看出,当执行select语句后会产生一个很是大的事务ID,那能不能理解为这种差别很是大的事务ID是经过快照读的方式才会生成的。
接着再这个事务下面在执行一个insert语句,而后再查看一下事务ID的状态
难以想象的是在事务中先执行select语句,而后执行insert语句,事务ID发生了变化,这是什么缘由呢?
通过资料查询得知当执行一个简单的select语句时,被称之为只读事务,为了不给只读事务分配trx_id带来没必要要的开销就没有对其分配事务ID。只读事务没有分配undo segment也不会分配LOCK锁结构,本质上只读事务的trx_id的值就是0,可是为了执行select * from information_schema.INNODB_TRX
或者show engine innodb status
时就会经过reinterpret_cast(trx) | (max_trx_id + 1)将指针转换为一个64字节非负整数而后位或(max_trx_id + 1) 就是这么个值。
关于这个值的生成过程就不用再去深究了,只须要知道在只读事务下是不会分配事务ID,而查询出来的这个值只是为了显示而存在的没有实际意义。
可是当你执行select * from information_schema.INNODB_TRX
查询出来的事务ID,再经过show engine innodb status
查询是查不到的。在Innodb下若是事务为只读事务则不会在Innodb数据结构中显示,所以你是看不到的。
坚持学习、坚持写做、坚持分享是咔咔从业以来一直所秉持的信念。但愿在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。