MVCC (Multi-Version Concurrency Control ,多版本并发控制)
指的就是在使用READ COMMITTD 、REPEATABLE READ 这两种隔离级别的事务在执行普通的SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
1.版本链
聚簇索引叶子节点的每条用户记录中都包含两个必要的隐藏列:
事务id(trx_id) :每次一个事务对某条聚簇索引的用户记录进行改动时,都会把该事务的事务id 赋值给trx_id 隐藏列。
回滚指针(roll_pointer) :每次对某条聚簇索引的用户记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
比方说我们的表hero 现在只包含一条记录:
| number | name | country |
|---|---|---|
| 1 | 刘备 | 蜀 |
假设插入该记录的事务id 为80 ,那么此刻该条记录的示意图如下所示:
假设之后两个事务id 分别为100 、200 的事务对这条记录进行UPDATE 操作,操作流程如下:
说明:当事务100更新这条数据时是加锁的,只有等执行完释放锁后,事务200才能继续更新
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer 属性( INSERT 操作
对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链
表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer 属性连接成一个链表,我们把这个链表称之为**版本链**(版本链就相当于是某条记录的变更历史),版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id ,这个信息很重要,我们稍后就会用到。
undo log 何时删除?
当某个事务对某条数据进行修改并成功提交后,生成的undo log不会被用来回滚已提交事务的数据,但是,这些undo log不会立即删除,还会被保留一段时间,以支持MVCC使用。如果所有活跃的读事务(即那些可能需要访问旧版本数据的事务)都已经结束,那么undo log就不再需要为这些事务提供旧版本数据了。此时,undo log可以被清除。
2.readview(读视图)
对于使用读未提交隔离级别的事务来说,由于可以读取到其他事务修改过但未提交的记录,所以直接读取版本链中最新数据就好了。
对于使用串行读隔离级别的事务来说,使用加锁的方式来访问记录(事务开始前记录是什么样的读到的记录就是什么样的),因此,其他事务无法对访问的记录进行修改,直接读取最新的数据就好了。
对于使用读已提交和可重复读隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,但是已提交的事务修改后的记录可能存在多个,形成版本链,所以,需要判断下版本链中哪个版本是对当前事务可见的。
于是提出了一个readview的概念,readview是一个数据结构,主要包含四个字段:
m_ids :表示在生成ReadView 时当前系统中活跃的读写事务的事务id 列表。
min_trx_id :就是m_ids 中的最小值。
max_trx_id :表示生成ReadView 时系统中应该分配给下一个事务的id 值(预分配事务id:最大事务id+1)。
creator_trx_id :表示生成该ReadView 的事务的事务id 。
有了这个readview后,只需要按照以下4个步骤来判断每个版本是否可见:
- 判断被访问的版本的事务id是否等于creator_trx_id,如果相等,说明当前事务在访问自己修改过的记录,该版本是可见的
- 判断被访问的版本的事务id是否小于min_trx_id,如果小于,说明生成该版本的事务已经提交了,该版本是可见的
- 判断被访问的版本的事务id是否大于max_trx_id,如果大于,说明生成该版本的事务在当前事务生成readview之后才开启,该版本是不可见的
- 判断被访问的版本的事务id是否在[min_trx_id, max_trx_id]之间(能经过上面3次判断后来到第4步,就只剩之间),此时肯定在[min_trx_id, max_trx_id]之间,再判断是否在m_ids列表中,如果在,说明创建readview时生成该版本的事务还是活跃的,该版本是不可见的;如果不在,说明创建readview时生成该版本的事务已经提交了,该版本是可见的
经过以上4步判断后,如果某个版本的数据对当前事务还不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,如果最后一个版本也不可见,那么就意味着该条记录对该事务完全不可见,查询结果中就不包含该记录。
读已提交 和可重复读 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同:
读已提交:是每次读取数据前都生成一个readview,同一个事务里前后两次读取数据时生成的readview可能不同,这就导致同一条记录前后两次读取的内容不一致,产生了不可重复读的问题。
可重复读 :只在第一次读取数据时生成一个readview,之后的查询就直接复用这个readview了,同一个事务里前后两次读取数据时用的是同一个readview,读取到的内容也必定相同,就不会产生不可重复读的问题。
3.快照读和当前读
MVCC只在普通select查询时才生效,上面描述的都是在普通select查询时的情况,普通select查询使用的就是快照读。换句话说,mvcc只支持快照读
3.1.快照读(一致性读或一致性无锁读)
事务利用MVCC 进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT 语句( plain SELECT )在READ COMMITTED 、REPEATABLE READ 隔离级别下都算是一致性读
快照读(一致性读)就是读取的是数据的一个历史版本,每个版本就像一个快照,所以也叫快照读。这种快照读不会对读取的数据进行加锁,即使其他事务正在对数据行进行更改,也不会导致读操作被阻塞。
3.2.当前读
当前读是指事务在读取数据时获取的是数据的最新版本,并且这种读取操作会对数据行进行加锁。当前读确保当前事务读取到的数据是最新的,并且在当前事务持有锁期间,防止其他事务对这些数据行进行修改。
当前读的例子包括:
- SELECT ... FOR UPDATE
- SELECT ... LOCK IN SHARE MODE
- UPDATE
- DELETE
- INSERT
这些操作不仅读取数据,还可能对数据行进行修改或者加锁,以确保数据的一致性。
4.MySQL怎么解决幻读?完全解决幻读了吗?
快照读:在可重复读的隔离级别下,通过mvcc保证了每次读到的都和第一次读到的数据是一致的,即使中途有新数据插入,也是看不到的,很好的避免了幻读问题。
当期读:通过临键锁(间隙锁+记录锁)的方式解决了幻读,当执行select ... for update 语句时,会加上临键锁,其他事务在临键锁的范围内插入数据的话就会被阻塞,于是就避免了幻读的问题。
MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。否则也不会有串行读了
特殊情况:如果在当前事务的两次快照读之间出现了当前读,就会重新生成readview,导致出现幻读的现象