我们知道,读已提交(Read Commited, RC)
和可重复读(Repeatble Read, RR)
这两种事务隔离级别,其默认的读的方式,是走的MVCC(Muti Version Concurrent Control, 基于多版本的并发控制,也叫基于一致性的非锁定读)
。在RC和RR的级别下的读操作,只有加了lock in share mode(s锁,shared lock,共享锁)和for update(x锁,exclutive lock,排他锁),才走LBCC(Lock Base Concurrent Control,基于锁定的并发控制,也叫基于一致性的锁定读)
。
在RC和RR的隔离级别下,假设我们开启一个事务,不使用LBCC的锁,在时间点A进行一个查询,过了很久以后,又在时间点B进行一个查询,哪怕这两次查询是select * from table where id = XXX的SQL,这样的SQL会不会慢呢?
答案是在业务上可能成为慢SQL,而且相比其他普通短事务的相同SQL查询,这样长事务中,时间点B(时间靠后)的查询SQL,一定是要更慢的。为什么会这样呢?
这是因为MVCC的多版本中的版本,指的就是undo log
。每一次进行写操作,都会生成一个undo log,undo log相当于一个数据快照,并且相比表的记录多了两个字段,roll_ptr
,指向上一个undo log(因此形成了一个undo log链表
,过期的undo log由mysql的perge线程
回收),trx_id
,形成这个undo log的事务的id(每一个事务都有自己的id,不同的事务形成的undo log都在同一个undo log链表中,由trx_id区分事务)。
而RR和RC级别的走MVCC的查询,都会根据Read View
找到自己可见的版本快照,在undo log链表中找到最近的可见的一个undo log作为查询的返回值。这里需要先解释一下Read View,
**m_ids:**表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
**min_trx_id:**表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
**max_trx_id:**表示生成ReadView时系统中应该分配给下一个事务的id值。注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
**creator_trx_id:**表示生成该ReadView的事务的事务id。
读已提交,是在每次读取时,都生成一个Read View,这样根据m_ids列表中的活跃事务(这些活跃事务的版本快照,属于未提交的脏数据,不读取),去undo log链表中找到可见的版本快照,这样就避免了脏读问题。
但是因为每次读操作都生成版本快照,存在不可重复读问题。如何产生的呢?比如一个事务A,先查了一次,生成一个Read View,查出相应数据;然后另外一个事务B开启,修改了事务A查询出的数据的值,并且提交了;最后,事务A再次查询,生成一个Read View,注意此时事务B已经关闭,不再活跃,也就是说事务B的版本快照对于事务A是可见的,那么事务A再次查询得到的值,就和一开始查询得到的值不一样了,这就是所谓的不可重复读问题。
解决这个问题的方式就是可重复读级别下,在第一次查询时生成一个Read View,后面的查询不再生成Read View,直接使用第一次查询生成的Read View,确保可见的事务列表不会发生改变,因此避免了不可重复读问题。
注1:ReadView在查看undo log时,会先查看undo log的trx_id是否在min_trx_id和max_trx_id之间。
如果在min_trx_id和max_trx_id之间,再继续看是否在m_ids列表中,如果在m_ids列表中,表示该生成undo log的事务是在生成ReadView时就在活跃的事务,这样的数据属于脏数据,不能读;如果不在m_ids列表中,表示在生成Read View时,生成该undo log的事务不活跃,即已提交,此时该undo log可以正常读取。
如果undo log的trx_id小于min_trx_id,表示生成这个undo log的事务的id,小于生成ReadView时活跃事务的最小id,也就是说它是不活跃事务生成的,即生成ReadView时该事务已提交。数据可以读取。
如果undo log的trx_id大于max_trx_id,表示生成这个undo log的事务的id,大于生成ReadView时活跃事务的最大id,也就是说生成RedView时,生成undo log的事务还没有开启,即生成ReadView后这个事务才开启。这样的undo log不能读,因为你要读的是最近一次符合查询条件的版本数据,不是未来的数据。