在这里,会涉及到一个概念:两阶段加锁协议。
之后,我会介绍行锁中的S锁和X锁,以及这两种锁的作用。
但是我们会发现仅仅有行锁是不能解决幻读问题的,于是我会用例子的方式跟你介绍各种间隙锁。
最后,我会聊一聊粒度更大的表级锁和库锁。
1 行锁在上一篇的文章中,我们用了这个具体的例子来解释MVCC:
假设我们调换一下T5和T6:
此时,T5是没有办法执行的。
原因是这样的:InnoDB在更新一行的时候,需要先获取这一行的行锁。
但是,当一条语句获取了行锁之后,不是这行语句执行完毕就能释放锁,而是要等到这个事务执行完毕,才会释放锁。
这里涉及到了两阶段加锁协议:它规定事务的加锁和解锁分为两个独立的阶段,加锁阶段只能加锁不能解锁,一旦开始解锁,则进入解锁阶段,不能再加锁。
然后我们再来说说共享锁(S锁,读锁)和排他锁(X锁,写锁)。
对于共享锁来说,如果一个事务获取了某一行的共享锁,则这个事务只能读这一行数据,而不能修改,并且其他事务也可以获取这一行数据的共享锁,读取这一行的数据,同样不能修改数据。
对于排它锁,只能被某一个事务获取。并且在获取排它锁之前,这一行数据上不能存在共享锁。一旦某一个事务获取了这一行的排它锁,那么只有这一个事务可以对这一行数据进行读写操作,其他事务对这一行数据的读写操作都会被阻塞。
此外,不仅仅只有更新操作,插入、删除操作也会获取这一行数据的X锁。
在这里我还要再介绍这两个概念:“快照读”和“当前读”。
你可能还会有印象,在上一篇内容中,我提到了所有的更新操作都必须是“当前读”,现在可以解释原理了,在更新一行数据的时候,InnoDB会对需要更新的那行数据加上X锁,直接获取最新的那一行数据。
与之相对的是“快照读”,也就是MVCC中的数据读取方式,利用“快照”来读取数据的方式,可以极大的提高事务的并发度。
但是并不是说select语句就只能读取快照,它也照样可以给需要读取的数据加锁,来读取最新的数据。也就是说,select语句也一样可以“当前读”。
下面这两个select语句,就是分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁)。
mysql> select k from t where id=1 lock in share mode; mysql> select k from t where id=1 for update;注意,由于两阶段加锁协议的存在,如果你采用了一致性读,那么这个锁必须要等事务提交后才能解除。这是牺牲了并发度的一种做法。所以,如果所有的select语句,都加上了S锁,此时的“可重复读”,就变成了“序列化”。
2 间隙锁 2.1 幻读问题还记得我们上面提到过的幻读吗?
现在你应该能够理解幻读产生的原因了:因为在插入数据的时候,InnoDB采用的是当前读,而读取数据的时候,由于MVCC的存在,采用的是快照读,这就造成了幻读。
但是我们在上面又提到了,select语句也一样可以采用“当前读”。那么,这样能解决幻读吗?
答案是能解决其中一种情况的幻读。
比如我们在上一篇文章中举的关于幻读的例子:
现在你能理解了,因为这里的select是快照读,而事务B的插入操作对于事务A来说是不可见的。如果在T5时刻,事务A的sql语句是select * from t where v = 0 for update,即采用当前读的话,是可以看得到事务B所提交的数据的,这样的话,就避免了幻读的情况。
那如果在T2时刻,事务A的语句就是select * from t where v = 0 for update会怎么样的?
如果在T2时刻就使用了“当前读”,那么T3时刻事务B是无法进行插入操作的。你可以理解为,T2时刻,InnoDB把v=0的数据,都给加上了一把锁。
因为这行sql语句把v=0的数据行都锁住了,所以没有办法再插入一行v=0的数据。