什么是幻读
以下例子都默认可重复读的隔离级别下
CREATE TABLE
t(
idint(11) NOTNULL,
cint(11) DEFAULTNULL,
dint(11) DEFAULTNULL,PRIMARY KEY (
id),KEY
c(c)) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25)
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
InnoDB通过间隙锁和行锁的方式解决了幻读,下面举2个例子来反证为什么需要间隙锁(实际中不会存在的)
1、假如只锁当前行
会发现q3会查到新增的1,1,5。产生了幻读(q2不能算幻读,幻读专指新增或删除的记录)
幻读产生的问题
1、语义上的
q1表示将所有d=5的记录锁住,但是没有锁住id=0的,所以session b的2条update语句可以执行。
session B的第二条语句update t set c=5 where id=0,语义是我把id=0、d=5这一行的c值,改成了5。
这样,就破坏了 session A 里Q1语句要锁住所有d=5的行的加锁声明。session c也是一样
2、数据一致性
现在,我们来分析一下图3执行完成后,数据库里会是什么结果。
-
经过T1时刻,id=5这一行变成 (5,5,100),当然这个结果最终是在T6时刻正式提交的;
-
经过T2时刻,id=0这一行变成(0,5,5);
-
经过T4时刻,表里面多了一行(1,5,5);
-
其他行跟这个执行序列无关,保持不变。
这样看,这些数据也没啥问题,但是我们再来看看这时候binlog里面的内容。
-
T2时刻,session B事务提交,写入了两条语句;
-
T4时刻,session C事务提交,写入了两条语句;
-
T6时刻,session A事务提交,写入了update t set d=100 where d=5 这条语句。
会发现binlog记录的顺序是有问题的,如果拿这个binlog去执行会和数据库的数据产生不一致的问题。
发现锁上当前行会产生幻读,那么锁上所有行呢?
假如锁上所有扫描的行
binLog执行顺序如下
insert into t values(1,1,5); /(1,1,5)/
update t set c=5 where id=1; /(1,5,5)/
update t set d=100 where d=5;/所有d=5的行,d改成100/
update t set d=5 where id=0; /(0,0,5)/
update t set c=5 where id=0; /(0,5,5)/
可以看出锁住了所有行,所有session b会被阻塞住,那么它在binlog中的顺序和执行的顺序保持一致,那么不会产生一致性问题。但是加锁的时候id=1这行都不存在,那么也就加不上锁,自然也阻止不了他的新增,那么还是会产生幻读问题。
间隙锁(GapLock)
行锁解决不了幻读问题,那么通过锁住可能会插入的空隙,来阻塞住update或者insert语句。既然阻塞住了更新和新增语句,那么自然也不会有幻读问题了。 间隙锁就是锁住2个值间的空隙
执行 select *fromt where d=5 for update的时候,就不止是给数据库中已有的6个记
录加上了行锁,还同时加了7个间隙锁。这样就确保了无法再插入新的记录。
与读写锁不同的是,间隙锁和间隙锁之前是没有冲突的(不像读锁和写锁之间冲突)。跟间隙锁存在冲突关系的,是 跟 “往这个间隙中插入一个记录” 这个操作,如果插入的记录在被锁住的范围,那么会冲突。
next-key lock:间隙锁+行锁
如:select *fromt for update要把整个表所有记录锁起来,就形成了7个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
加锁规则
- 原则1:加锁的基本单位是next-key lock。next-key lock是前开后闭区间。
- 原则2:查找过程中访问到的对象才会加锁。
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。