MySQL在底层使用共享锁、排他锁以及区间锁去实现不同的事务隔离等级,共享锁和排他锁不必多说,类似于读写锁,而对于区间锁而言它是为了部分解决幻读而提出的且仅能用在RR隔离级别上
区间锁的范围如下:根据检索条件C向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,C) + {C} + (C, B),这里注意两边都是开区间,另外需要注意锁是加在索引上的,也即有没有被锁阻塞是看加了锁的索引的那一列相同不相同或者是否落在了区间里,另外对于区间锁而言在相同的情况下(边界情况)会额外去看主键的顺序是否也落在了区间里,如下图所示,其中没有gap lock的行是原始行,在select了age=22后的区间锁是(id=5 age=18, id=20 age=28)
在事务隔离机制的基于锁的实现中Read Committed对于读上读锁然后马上放锁,对于写则上写锁一直到事务结束;Repeatable Read对于读上读锁,对于写上写锁一直到事务结束;Sequential则三个锁都上。
为了进一步提升性能,在Read Committed和Repeatable Read中使用了MVCC替代了读锁,从而解决了读读、读写以及写读的并发问题,也即当读被其他写操作锁定的数据时会去读之前的版本,但是对于写写的情况依然会阻塞到另外一个事务提交为止。另外当事务提供的隔离等级不满足业务要求时可以主动地进行上锁(lock in share mode和for update)。
更新数据x=x+1都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read),也即总是读取已经提交完成的最新版本并且在提交前加锁(两阶段锁)。除了 update 语句外,select 语句如果加锁,也是当前读。
MVCC主要基于版本链和Read View实现
版本链是由于在每次对于某条记录进行了修改操作之后,并不会直接删除原记录,而是暂时保存成Undo Log并且在新纪录中通过 roll_pointer指向旧记录,且每条记录都会通过 trx_id记录是哪个事务进行了当前的更改。
在Read View中主要存放了4个比较重要的内容:1. m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。2. min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值 3. max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。4. creator_trx_id:表示生成该ReadView的事务的事务id。
通过Read View在访问某条记录时则可以通过以下步骤判断记录的某个版本是否可见:1. 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。2. 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。3. 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。4. 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
事务的级别只是针对读的一致性而言的,如果出现写操作,则必定会加锁,普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。
Read Committed和Repeatable Read的差别在于,前者在每次查询时都会生成一个新的Read View,而后者只在事务开始且第一次读查询时生成一个Read View