mvcc原理详解

前提:在介绍mvcc之前,先简单介绍一下mysql事务的相关问题,mvcc归根结底是用来解决事务并发问题的,当然这个解决不是全部解决,只是解决了其中的一部分问题!
mysql事务
一、事务的基本要素(ACID)
  1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
   2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
   3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
   4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

二、事务的并发问题
  1、脏读:事务A读取了事务B未提交的数据,然后B回滚操作,那么A读取到的数据是脏数据 ----解决的基础隔离级别:读已提交
  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。----解决的基础隔离级别:可重复读
  3、幻读:是针对数据插入操作来说的,假设在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同(事务B在事务A两次查询期间插入符合查询条件的新数据),这种现象称为幻读。—解决的基础隔离级别:串行化(针对一张表的操作,每一个session依次完成自己的操作:行锁 + 间隙锁)MySQL事务隔离级别和实现原理
  小结:不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。

三、MySQL事务隔离级别

mvcc原理详解

MVCC多版本并发控制

  • 前提概要
    • 什么是MVCC
    • 什么是当前读和快照读?
    • 当前读,快照读和MVCC的关系
    • MVCC能解决什么问题,好处是?
  • MVCC实现原理
    • 隐式字段
    • undo日志
    • Read View(读视图)
    • 例子
  • MVCC相关问题
    • RR是如何在RC级的基础上解决不可重复读的?
    • RC,RR级别下的InnoDB快照读有什么不同?

一、前提概要
什么是MVCC?
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

什么是当前读和快照读?
在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?

  • 当前读
    像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

  • 快照读
    简单的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

MVCC能解决什么问题,好处是?
数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题

MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该session的数据库的快照。 所以MVCC可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
    二、MVCC的实现原理
    它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。

隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  • DB_ROW_ID
    6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
  • 实际还有一个删除flag隐藏字段, 既记录被删除并不代表真的删除,而是删除flag变了
    mvcc原理详解

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

undo日志
undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除

对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

一、 比如有个事务在persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL。为演示,插入提交后,该undo log被删除
mvcc原理详解

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们假设从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁
    mvcc原理详解

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 在事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交,释放锁
    mvcc原理详解

记录版本链

从上面,我们就可以看出,同一记录的多次修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

Read View(读视图)
什么是Read View?(对照表?)

  • 什么是Read View,Read View是session进行快照读操作的时候生产的读视图(Read View),在该session执行的快照读的那一刻,会生成数据库系统当前的一个快照。
  • Read View主要是用来做可见性判断的, 即当执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前session能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View:执行查询时【所有】未提交的事务Id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id:未提交、已提交)组成
mvcc原理详解

mvcc遵循一个可见性算法,查询时候,需要用read-view和undo log 做对比得到结果
比较规则:
依次比较记录版本链中每一条记录,符合规则就返回该条数据,不符合根据回滚指针取链路中的下一条数据;
trx_id为记录版本链里的DB_TRX_ID
1、如果trx_id < min_id(落在绿色部分),表示这个版本是已经提交的事务生成的,这个数据肯定是可见的;
2、如果trx_id > max_id (落在红色部分),表示这个版本是由将来的启动的事务生成的,是肯定不可见的;
3、如果min_id <= trx_id <= max_id (落在黄色部分),那就包括两种情况
a、若trx_id在未提交的事务Id数组里,表示这个版本是由未提交的事务产生的,不可见,当前自己的事务是可见的;
b、若trx_id不在未提交的事务Id数组里,表示这个版本是已经提交的事务生成的,可见

对于删除的情况,可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将其trx_id修改成删除的trx_id,同时在该条记录的头信息(record_header)里的(delete_flag)标记位上写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录,如果delete_flag标记位等于true,意味着该条记录已被删除,则不返回数据

例子
默认RR级别
https://www.processon.com/view/link/5eef4a2d6376891e81dc7d28

三、MVCC相关问题
RC,RR级别下的InnoDB快照读有什么不同?
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

  • 在RR级别下的某个session对某条记录的第一次快照读会创建一个Read View, 将当前系统活跃的其他事务记录起来,此后再快照读的时候,还是使用的是同一个Read View,换一句话说,只要当前session在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
  • 而在RC级别下的,每次快照读都会新生成一个Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
    总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个session中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

可参考文档
mvcc https://www.jianshu.com/p/8845ddca3b23
bin log、redo log、undo log区别 https://www.cnblogs.com/wq-blogs/p/11867199.html
事务 https://blog.csdn.net/qq_38997379/article/details/89017287
详细解读mysql 的undo log https://blog.csdn.net/qq_39459385/article/details/84644005
详细分析MySQL事务日志(redo log和undo log)