最近学习了mysql的各种锁,有点晕,打算通过文章的方式捋一捋。
在学习了mvcc后,我就想,他已经很好的解决了并发读写了,但我也知道innodb提供了多种类型的锁,所以很好奇这些锁有什么用,为什么这些锁的功能是mvcc做不到的?(本文讨论的都是rr级别下的锁)
我先创建一个表,并插入几行数据,如下图:
插入内容如下:
c字段加了普通索引,d字段无索引。
此时,开启session a,开始一个事务,搜索d=5的记录,同时开始session b执行更新操作,session c执行插入操作。
现象:session b 的update和session c的 insert都被阻塞了,注意,并没有直接返回错误。
为什么会被阻塞呢?
因为session a用了select for update这个锁,是排他锁。
在研究这个排他锁之前,我先从语义上来理解select * from t where d=5 for update的含义。 这句sql的含义是锁住d=5的行吗?也许是的。其实也是的。但是这里有个问题,如果不存在的行,怎么控制呢?session a一次事务中多次执行elect * from t where d=5 for update我们是不是希望得到同样的记录(除非本事务内自己修改)?如果只是锁住了d=5这行,那么session B的update和session C的insert就会在session A的事务进行时插入、更新,这样session a会后面select for update会发现多出来了d=5的行,和第一次不一样,这就破坏了session A锁的声明。这就是幻读。
什么是幻读?
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
- 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
- 上面 session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。
怎么解决呢?
通过上面的讨论我们得知通过给d=5这行加锁是不行的,那么如果我们给所有扫描的记录都加锁能解决这个问题吗?如果这样,update语句会被阻塞,但是insert语句依然可以执行成功,因为insert这行是新的行,我们并没有办法给他加锁。也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录
到底如何解决?
接下来,我们再看看 InnoDB 怎么解决幻读的问题。
我们知道行锁只能锁住现有的行,阻止update,但是不能阻止新的行插入,所以innodb引入了间隙锁gap lock,这样在其他session 执行select * for update时,插入操作就会被block。
下图就是六条记录,并且形成了7个间隙,因此有7个间隙锁。
由于d没有索引,所以所有扫描到的行和间隙都会加上锁,此时其他session就不能做任何插入和更新操作了。为了减小锁的粒度,一般需要在索引上做select for update,比如select * from t where c=5 for update。
我们知道,行锁分为读和写两类锁,那么同一个记录写行锁和写行锁是冲突的,这符合行锁的语义。间隙锁呢?不同session的对同一个间隙加锁,会冲突吗?先来分析一下间隙锁的语义,比如(5,10)这个间隙锁,语义是阻止5-10内的插入。所以不同的session对同一个间隙加锁是服务语义的,不冲突。当然也会导致死锁。比如如下顺序操作:
上图的执行顺序会造成死锁:
1、session A先锁住了(5,10),然后session B 也锁住了(5,10)。
2、session B试图插入(9, 9, 9),但是他和session A的间隙锁冲突了,所以会被block
3、接下来session 试图插入(9, 9, 9),和session B的间隙锁冲突,也会被block,就形成了死锁。
间隙锁的引入解决了rr级别下的幻读,但是也导致锁的范围变大了,不同时刻,同一个select 语句可能锁住不同的间隙,的确影响了并发度,但这也是无奈之举。行锁加上间隙锁就是next-key-lock,当select * from t where c=x for update,这个x在表中时就会加next-key-lock。
值得注意的是,只有rr才有间隙锁,读提交的情况下,是不会有间隙锁的,因为提交后必须被读到,隔离性没那么好。