谈到MYSQL事务,必然绕不过INNODB的MVCC机制,也是面试中常考常问的问题之一,不过网络上关于MVCC的文章或者公开课零零散散讲了很乱,听得我迷迷糊糊的,这里对我对这些资料进行一次总结,加深我的印象,也希望对看到的人有所帮助(不要浪费你们的时间就好了hhh)

MVCC,中文名字是多版本并发控制,说白了就是数据库并发的一种手段(更合理应该说提高数据库并发下的性能,相比于直接加锁确实性能提升了很大),它的最大优势是 读不加锁,读写不冲突

因此在说明MVCC如何实现之前,我们需要先了解一下数据库的并发事务带来的问题,事务隔离级别等基础知识。

一. 并发事务带来的问题

这里我就偷懒了,直接拿《阿里巴巴java性能调优》一书中并发事务安全一节

二. 事务隔离级别

  • 读未提交:可以读到别的事务未提交的数据修改
  • 读已提交(RC):读取时可以读取到已提交事务的数据
  • 可重复读(RR):一个事务执行过程中看到的数据,总是和这个事务启动时数据保持一致
  • 串行化:读加S锁,写加X锁

RC和RR的区别

假设账户上初始资金是1000元

Transaction A Transaction B
BEGIN
SELECT money FROM Account (1)
BEGIN
UPDATE Account SET money=money+1000
SELECT money FROM Account (2)
COMMIT
SELECT money FROM Account (3)
COMMIT

假设数据库是RC级别,那么(2)看到的是1000元,(3)看到的是2000元

若数据库是RR级别的,那么(2)(3)看到的都是1000元

三. MVCC性能优势提升

在JAVA多线程中,也有S锁和X锁,除了S锁和S锁可以共活,其余的都要排斥进行阻塞,但是MVCC不同,MVCC对普通的SELECT不加锁,如果读取的数据正在DELETE或者UPDATE操作,这时候读取操作不会等待X锁释放,而是直接读取该行记录的数据快照,而这依靠的是MYSQL的日志系统中undo日志。

所以,MVCC的优势是避免了对数据重复加锁过程,适合了读多写少的场景。

四. MVCC的实现原理

所谓多版本就是历史记录,这依靠的就是undolog日志,而同一行记录的不同历史版本是通过两个重要字段进行区别和链接的:DB_TRX_IDDB_ROLL_PT,从而让不同记录串起来,这就是undolog链

img

​ 除了undolog之外,MVCC实现还离不开另外一种机制,也就是ReadView机制,这是MVCC保证RC或者RR隔离级别的手段,所谓ReadView就是innodb为每一个事务生成的一个数组,数组里保存的是这一瞬间(RC和RR不一样)活跃事务的id(活跃事务就是已开始但未提交的事务)

img

低水位就是数组中最小的事务id,而高水位指得是当前创建事务ID最大值+1

ReadView和undolog链是这样配合的:

  1. RR级别下只有第一个SELECT会产生ReadView,RC级别下每次SELECT都会产生ReadView
  2. 根据上条规则产生ReadView,然后遍历这条行记录的历史记录的DB_TRX_ID,如果DB_TRX_ID<低水位,那么说明是已经提交的,这条旧记录则可以访问
  3. 如果历史记录的DB_TRX_ID大于高水位,则说明这个事务是在ReadView之后产生的,那么这条记录是不可以访问的
  4. 如果历史记录的DB_TRX_ID是当前活跃事务,也就是ReadView产生的数组中的一员,那么要分成两种情况,如果是当前事务(举个例子,事务ID为200,访问的记录的事务ID也是200)那么这是可以访问的,如果不是则不能访问

这里就回答了之前RC和RR的区别那一点的区别了,因为RC是每次SELECT都会产生ReadView而RR只有第一次才会产生ReadView,所以并不会认可历史记录(即便真的有这个历史记录)

但又会产生新的问题,如果RR每次都不拥抱变化,那么一个事务包括读写操作时不就会造成并发事务问题中数据丢失问题了吗?(换句话说看不到之前事务的变化)

如:

Transaction A Transaction B
BEGIN
SELECT money FROM Account (1)
BEGIN
UPDATE Account SET money=money+1000
SELECT money FROM Account (2)
COMMIT
SELECT money FROM Account (3)
UPDATE Account SET money=money+1000
SELECT money FROM Account (4)
COMMIT

即(4)是1000还是2000还是3000呢?答案相信经历过我国多年培养出来的做题直觉也知道是3000,这是因为UPDATE不会使用一致性读的方式(也就是上面产生ReadView数组的方式),而是采用当前读的方法读取最新的数据(还是要依靠undolog链)并且要加锁,这也就解决了数据丢失的问题。

五.参考资料

  1. MySQL45讲https://funnylog.gitee.io/mysql45/
  2. 强推该文https://seven.geekfun.club/article/16
  3. 图片来自网络