【元婴境】mysql的MVCC(详解)

目录

MVCC介绍

隐藏字段

undo log

ReadView

示例:

流程总结


MVCC介绍

         大家好,我是jstart千语。上篇我们讲到mysql的事务隔离级别,其中MVCC就是控制事务隔离级别的重要组成部分,也是实现事务四大特性之一隔离性的重要手段。那么接下来我将通透地讲解MVCC,让大家对mysql的隔离性有一个更深刻的理解。

        MVCC全称 Multi-Version Concurrency Control,也就是多版本的并发控制,这一点从名字就可以感受到。具体指维护数据的多个版本,在并发环境下,控制数据的读写没有冲突。简单来说解决的就是:其他事务不断进行修改数据时,当前事务在不同时间段读取的是什么时候的数据?

        实现的手段依赖于:隐藏字段、undo log日志、readView,接下来逐一讲解。

 


 

 隐藏字段

每次创建表的时候,在mysql的默认存储引擎innoDB下,mysql都会自动地给我们的表添加三个隐藏字段,分别是:DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。

DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。(默认从1开始,每次修改都会+1,是自增的)
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undolog,指向上一个版本。
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。


 

undo log

        undo log是一个回滚日志。有两个重要的功能,一个是提供事务的回滚,一个是现在的MVCC

  • 每当有一条insert语句执行时,undo log就会记录一条对应的delete记录,用于后面的回滚,反之执行delete语句时亦然。所以这里可以看到 undo log 其实是一个逻辑日志。
  • 当执行一条update语句时,也会记录一条相反的 update 记录。当执行rollback时,就可以从 undo log 中读取到相应内容并回滚。
  • 注意点:执行insert时,日志的记录只会在回滚时需要,如果commit提交了,记录就会被立即删除;但是,执行update和delete时,参数的日志不仅回滚时需要,MVCC也要访问,不会立即被删除
  • undo log的版本链:不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是 最早的旧记录。这里是使用隐藏字段中的回滚指针实现的

eg:当某一条数据,依次被多个事务修改时,回滚指针会记录上一次提交事务时的日志的地址。这就是undo log 的版本链。

【元婴境】mysql的MVCC(详解)_第1张图片 每一条数据的更改都记录着一个数据的版本


 

ReadView

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的事务)id

当前读和快照读的区别:

  • 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,select...lock in share made(共享锁)、select...for update(排它锁)
  • 简单的select(不加锁)就是快照读,快照读读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
    • 快照读的生成又区别于不同的隔离级别。
    • Read Committed:每次select都会生成一个快照版本
    • Repeatable Read:开启事务后所有select都是复用第一个select生成的快照版本

        每个readView都会记录一些核心的字段,这些字段逐一(参考上面的版本链)对不同版本数据(含未提交)的事务id进行计算比对,看看是否满足某一条件。如果不满足,校验上一个版本的数据是否满足条件。如果满足,那么就表明当前的这个readView(select语句)可以读到这条版本的数据。

ReadView的核心字段:

m_id 当前活跃的事务ID集合,就是指未提交的事务
min_trx_id 最小活跃事务id,事务id最小的事务(id是自增的)
max_trx_id 下一个即将分配的事务id,当前最大事务id+1
creator_trx_id ReadView的创建者事务id

每个readview的生成都会有上面四个核心字段。计算的规则如下

【元婴境】mysql的MVCC(详解)_第2张图片 每一条数据的更改都记录着一个数据的版本

事务 ID 可见性规则

对于某条数据的版本(由 trx_id 标识),例如上面的版本链中trx_id首先等于4,判断其是否对当前事务可见的规则如下:

1. 若 trx_id == creator_trx_id

  • 可见
    该版本由当前事务自身修改,无论是否提交,当前事务均可直接访问。

2. 若 trx_id < min_trx_id

  • 可见
    该版本对应的事务在 ReadView 生成时已经提交。

3. 若 trx_id >= max_trx_id

  • 不可见
    该版本对应的事务在 ReadView 生成后才启动,属于未来事务的修改。

4. 若 min_trx_id ≤ trx_id < max_trx_id

  • 检查 trx_id 是否在 m_ids 中:

    • 存在:不可见(事务在 ReadView 生成时仍活跃,未提交)。

    • 不存在:可见(事务在 ReadView 生成时已提交)。

示例:

以上面那副图的版本链为例

【元婴境】mysql的MVCC(详解)_第3张图片 每一条数据的更改都记录着一个数据的版本

假设 ReadView 的字段为:

  • m_id  = [4,5]
  • min_trx_id = 4
  • max_trx_id = 6
  • creator_trx_id = 5

----------------------------------------------------------------------------------------------------------------------------------------------

判断依据:

1. 若 trx_id == creator_trx_id

2. 若 trx_id < min_trx_id

3. 若 trx_id >= max_trx_id

4. 若 min_trx_id ≤ trx_id < max_trx_id,检查 trx_id 是否在 m_ids

  • 数据版本 trx_id  = 4:上述条件均不满足,不可见,根据版本链到下一个数据版本
  • 数据版本 trx_id = 3:第一个判断不成立,但第二条trx_id

所以这个readView可以看见trx_id=3的版本的数据,也就是


流程总结

至此我们已经完成了MVCC对数据的可见性控制。简单来说就是:

  • 隐藏字段给undo log提供版本链的实现和给readView提供部分核心字段的计算。
  • 不同隔离级别的readView生成情况不同
    • 读已提交:每次select都会生成一个ReadView
    • 可重复读:只有第一次select才会生成ReadView,往后的都是服用第一个ReadView
  • undo log与readView搭配使用,对数据的版本逐一判断是否可见。

        所以,这里也就说明了,为什么读已提交会造成不可重复读,因为它每次select都会生成一个快照,当两个快照之间被修改了数据并提交了,那么两次快照里的核心字段是不一样的,所以计算出来的数据可见性也是不一样的。

你可能感兴趣的:(mysql,数据库)