幻读是什么
先创建一张表,并插入6个数据。
-- 除了主键索引外,还有索引c
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
插入6条数据
insert into t values
(0,0,0),
(5,5,5),
(10,10,10),
(15,15,15),
(20,20,20),
(25,25,25);
session A里执行了3次查询,分别是Q1,Q2,Q3,他们SQL相同,都是"select * from t where d =5 for update;",即查询所有d=5的行,使用当前读,并加上写锁。
- Q1值返回id=5这一行。
- T2时刻,session B把id=0这一行的d值改为5,因此T3时刻Q2查询出来的是id=0和id=5这两行。
- 在T4时刻,session C又插入一行(1,1,5),因此T5时刻Q3查询出来3条数据(id=0,1,5三条)。
在Q3读到id=1这一行的现象,就是"幻读"。幻读就是指同一个事务在前后两次查询同一个范围的时候,结果不一致。
- 可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的,因此幻读在“当前读”情况下才会出现。
- 上面session B修改的结果,在T3时刻被sessionA用当前读看到,不能称为“幻读”,幻读专指“新插入的行”。
上面三个查询都是加了for update,都是当前读。当前读的规则:要读到已提交的事务的最新值。
幻读有什么问题
问题1:破坏语义
sessionA在T1时刻就声明了:“要把所有d=5的行锁住,不准别的事务进行读写操作”,实际上,这个语义被破坏了。
- T1时刻,session A只是给(5,5,5)这一行加了行锁(where条件d=5),并没有给id=0这行加锁。
- sessionB在T2时刻,是可以执行对id=0的update语句的。
- 这样就破坏了sessionA里Q1语句要锁住所有d=5的行加锁声明。
问题2:数据一致性问题
- T1时刻,id=5(5,5,5)这一行变成了(5,5,100),当然这个结果是在T6时刻才提交。
- T2时刻,id=0(0,0,0)这一行变成了(0,5,5)
- T4时刻,表里面多了一行(1,1,5)
此时binlog里面的内容分析如下:
- T2时刻,session B事务提交,写入了2条update语句。
- T4时刻,session C事务提交,写入了1条update和1条insert语句。
- T6时刻,session A事务提交,写入了1条update语句(update t set d =100 where d=5)。
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=0; /*(1,1,5)*/
update t set d=100 where d=0; /*所有d=5的行,d都改成100*/
如果这个拿到备库去执行,或者是用binlog来克隆一个库,这3行的结果都会变成(0,5,100)、(1,5,100)、(5,5,100),即id=0和id=1这2行,发生了数据不一致。
如何解决幻读:间隙锁
行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的间隙,为解决幻读问题,InnoDB引入了新的锁:间隙锁(Gap Lock)。
初始化插入了6条数据,总共产生了7个间隙。
当执行select * from t for update时,不止给数据库中已有的6个记录加上了锁,还同时加了7个间隙锁,这样就确保无法再插入新的记录。即此时,在一行行扫描过程中,不仅给行加了锁,还给行两边的间隙加上了间隙锁。
跟间隙锁存在冲突的,是“往这个间隙中插入一个记录”这个操作,而两个间隙之间是不存在冲突关系,如下:
sessionB不会被堵住,因为表t里面并没有c=7这个记录,sessionA加的间隙锁(5,10),而sessionB也是在这个间隙加的间隙锁,他们之间并不会冲突。
间隙锁和行锁合称为 next-key lock。每个next-key lock都是前开后闭(]区间,如果用select * from t for update要把整个表所有记录锁起来,会形成(-∞,5]、(5,10]、(10,15]、(15,20]、(20,25]、(25,+surpermum]。
间隙锁和next-key lock的引入,帮我们解决了幻读的问题,同时间隙锁也可能会造成“死锁”问题:
- sessionA 执行"select * from t where id=9 for update;",由于id=9这一行并不存在,因此会加上间隙锁(5,10)。
- session B执行同样的语句select * from t where id=9 for update;",同样也会加上间隙锁(5,10),间隙锁之间并不会冲突,因此这个语句可以成功执行。
- session B试图插入一行(9,9,9),被sessionA的间隙锁挡住了,只好阻塞等待。
- session A试图插入一行(9,9,9),被sessionB的间隙锁挡住了,同样阻塞。
- 两个session进入互相等待状态,形成死锁,MySQL检查到之后会让sessionA的insert报错返回。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,其实是影响并发度的。
注意:间隙锁是在可重复读的隔离级别下才会生效,如果隔离级别是"读已提交",是没有间隙锁的,在读已提交的隔离级别下,需要解决可能出现的数据和日志不一致的问题,需要将binlog格式设置为row,这也是不少公司使用的配置组合。(如果业务认为读已提交隔离级别够用,不需要可重复读的保证,读已提交隔离级别锁的范围更小,这个选择是没问题的)