MVCC原理机制

回顾

什么是mvcc

多版本并发控制(MVCC),是一种用来解决冲突无锁并发控制,也就是为事物分配单项增长的时间戳,为每个修改保存一个版本,版本与事物戳关联,读操作只读事物开始前的数据库的快照,这样在读操作的时候,就不会阻塞写操作,写操作不会阻塞读操作的同时也避免了脏读和不可重复读

当前读和快照读

在学MVCC多版本并发控制之前,先了解一下什么事mysql Innodb下的当前读和快照读

当前读

当前读指的就是,读取记录是最新版本的。由于它尧都区记录是最新版本,所以读取是,必须保证其他事物不能修改当前记录,因此需要对读取的记录加行锁

快照读

快照读可以理解为不加锁的select操作就是快照读;快照读的前提是隔离级别不是串行级别,因为在串行隔离级别,快照读可以理解为当前读;快照读的出现主要解决了在不加锁的情况下也可以进行读取,降低了锁的开销,它的实现是基础的多版本并发控制,即MVCC;由于是基于多版本并发控制,所以使用快照读的记录并不一定是最新记录

和MVCC的关系

准确的说,MVCC主要基于维护一条数据的多个版本,进而保证在读操作的同时不会阻塞写操作,写操作的同时也不会阻塞读操作

快照读其实就是MVCC的一种体现方式,进行非阻塞读。相对而言,当前读就是悲观锁的体现,每次进行查询操作时,mysql都认为其是不安全的操作,为其加锁保证安全,但每次读取的都是最新的数据

对于会对数据修改的操作(update、insert、delete)都会执行当前读。假设要update一个记录,另一个事务已经delete这条数据并且commit了,这样就会产生冲突,所以update的时候肯定要知道最新的信息。
在执行修改数据的时候,首先会执行当前读,然后把返回的数据加锁,之后执行修改数据。加锁是防止别的事务在这个时候对这条记录做什么,默认加的是排他锁,也就是你读都不可以,这样就可以保证数据不会出错了。
想要手动执行当前读需要在后缀加for update,如

select * from test1 for update

总结:mysql查询默认执行快照读,修改执行当前读操作

实现原理

MVCC模型在mysql中具体主要是由隐藏字段,undolog,read-view完成的,具体实现看下面的MVCC实现原理

隐藏字段

隐藏字段中除了咱们定义的字段外,还隐含着其他字段,是系统默认给加上去的,比如poll——pointer,trx_id等字段

roll_pointer

回滚指针,指向这条记录的上一个版本

trx_id

事物ID,记录创建、修改这条记录的事物id,用于版本比较,从而找到快照

nameagetrx_idroll_pointer(回滚指针)
迈莫2210x1654u

如表中所示,name和age属性为用户自定义属性,而trx_id和roll_pointer就表示隐藏属性,数据库默认添加。在这表中,trx_id表示操作这条记录的事务ID,roll_pointer是回滚指针,表示指向上一个版本,一般配合undolog日志使用

undolog日志

undolog日志存储某条记录的所有操作,以链表方式将各个版本进行串联起来

例子

第一步: 比如有个事务1向person表中插入一条数据,记录如下,name为迈莫,age为22,事务ID为1,回滚指针假设为null,如下图所示

第二步:此时又来一个事物2,对该记录的age值进行修改,修改为23

首先将事物1的操作记录到undolog日志中

将事物2的操作记录作为数据的最新记录

将事物2中隐藏字段roll+pointer(回滚指针)指向事物1,进行串联

第三部:又有一个事物3,对该记录的name值进行操作,修改为memolei

首先将事物2的操作记录迁移到undolog中

将事物3的操作记录作为数据的最新记录

将事物3中隐藏字段roll+pointer(回滚指针)指向事物2进行串联

从上面图可以看到,每当有事物对该数据进行操作时,首先将操作最新数据迁移到undolog日志中,将当前事物操作的记录作为最新数据,并且将隐藏字段poll_pointer指向上个版本

read-view一致性视图

当前事务第一次执行查询sql时会生成一致性视图read-view,它由执行查询所有未提交事物ID数组(数组里最小的ID为min_id)和已创建的最大事物id(max_id)组成,查询的数据结果需要跟read-view作比较从而得到快照结果

版本比对规则

  • 如果落在绿色部分(trx_id<min_id),表示这个版本是已提交的事物生成的,是课件的
  • 如果落在红色的部分(trx_id>max)表示这个版本是由将来启动的事物生成的,是肯定不可见的
  • 如果落在黄色部分(min_id<=trx_id<=max_id),就包含俩种情况

    • 若row的trx_id在数组中,表示这个版本是由还没提交的事物生成的,不可见,当前自己的事物是课件的
    • 若row的trx_id不在数组中,表示这个版本是已提交的事务生成的,是可见的

readView包含四个字段

  • m_ids:当前活跃的最小事物编号集合
  • min_trx_id:最小活跃事务编号
  • max_trx_id:预分配事物编号,当前最大事物编号+1
  • creator_trx_id:readView创建者的事物编号

整体流程

如图表所示,有四个事务,分别为translation2,translation3,translation4,translation5;translation2,translation3,translation4分别对数据库进行修改操作,translation5进行查询操作。

  1. 当translation2对persion表中ID为1的数据进行修改时,首先会将该记录作为最新记录,并且roll_pointer指向上个版本,但由于translation2未commit,所以translation2仍为未提交事物

  1. 当translation3对person表中的ID为1的数据进行修改时,首先会将记录操作作为最新记录,并且roll_pointer指向上一个版本,但因为translation3未commit,所以translation3仍然为未提交事物

  1. 当前translation对person表中ID为1的数据进行修改时,首先会将该记录作为最新记录,并且roll_pointer指向上一个版本,但由于translation4已经commit,所以transation4为已提交事务

  1. translation5进行select语句查询时,并且由于是第一次创建查询语句,所以会创建read-view一致性视图.read-view一致性视图是由未提交事物ID数组和最大事物ID组成,由表中可知,当translation5进行查询时,translation2和translation3都为未提交事务,translation4为已提交事物,所以,read-view中的未提交事物ID数组由translation2和translation3组成,最大事物ID为translation4,所以max_id为translation5,min_id为translation2
  2. 接下来的话,就需要进行版本链比较(若不记得版本链回去温习一下),由于read-view由未提交事物数据[1,2]和最大事物ID3组成.由于当前最新记录事务ID为4,4大于最大事物ID3,所以无法查看;进行回溯,到undolog日志查询,事务3在未提交事物中,也就是中间这一段,由于事务三不在未提交事务数组中,说明事务3为已提交事务,因此是可见的,最终结果为name="memoei"

在看一遍,这是提交读的情况

将每一个版本的数据代入到右侧的判断中,如果符合就返回当前数据,如果都不满足,就向下沿着版本链,获取到满足的结果位置

m_ids记录的是哪些事物还没有被提交,会把他之前的事物挨个放到右边的访问跪着中进行比对

所以第一个输出的是张三,第二个输出的是张小三

以下是可重复读的情况(RR)

TRX_ID1和TRX_ID2全都不满足,所以依次向下,直到TRX_ID1才全部满足,所以返回TRX_ID1的数据

所以RR能避免幻读,不能完全避免幻读,因为mvcc不是通过锁,而是通过版本控制的方式,变相实现了解决幻读的功能

连续多次快照读,readView会产生复用,没有幻读问题

特例:当俩次快照读之间存在当前读(修改操作),readview会重新生成,出现幻读

Last modification:November 17, 2023
如果觉得我的文章对你有用,请随意赞赏