3)T6 时刻,session A 事务提交,写入了 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=1; /*(1,5,5)*/
update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/
这是我们假设“select * from t where d=5 for update 这条语句只给d=5 这一行,也就是 id=5 的这一行加锁”导致的。
进一步假设:
把扫描过程中碰到的行,也都加上写锁,再来看看执行效果
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的问题,但是session C新增的问题并没有解决!
在 T3 时刻,我们给所有行加锁的时候,id=1 这一行还不存在,不存在也就加不上锁。也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录。
3.InnoDB 怎么解决幻读的问题
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (GapLock)。
间隙锁,锁的就是两个值之间的空隙。比如表 test_20,初始化插入了 6 个记录,这就产生了 7 个间隙。
当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。
数据行是可以加上锁的实体,数据行之间的间隙,也是可以加上锁的实体。但是间隙锁跟我们之前碰到过的锁都不太一样。
跟行锁有冲突关系的是“另外一个行锁”。
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
如下:这里 session B 并不会被堵住。因为表 test_20里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。
间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。也就是说,表test_20初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25,+suprenum]。
把间隙锁记为开区间,把 next-keylock 记为前开后闭区间。
3.1 带来的问题
1)session A 执行 select ... for update 语句,由于 id=9 这一行并不存在,因此会加上间隙锁 (5,10);
2)session B 执行 select ... for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此这个语句可以执行成功;
3)session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;