MVCC讲解:

时间:2024-12-14 07:07:19

什么是MVCC:

全称是多版本并发控制.他是为了提升并发能力的技术,解决了数据库操作是的读写和写读问题,注意他不能解决写写问题。如果是写写问题就还是互斥的形式。

在MVCC内部是给予InnoDB存储引擎的undo log 记录的信息实现的,每个事务读到的数据的版本可能是不一样的。在这个过程中,事务只能是看到自己创建事务时酷照的数据,和事务本身操作的数据。

MVCC 只是在RC(读已提交)和RR(读未提交)的隔离级别时使用

MVCC借助于三点来工作的,分别是:隐藏字段,undo log(注意这个日志文件本身是为了在事物的操作时,保证事务的原子性,所以他是在事务提交前就写入,只是和Redo log不同的点,在这里点出一下。),Read view (这个其实就是快照)

详细讲解隐藏字段:

InnoDB为每个数据库的表中添加了三个隐藏字段:

  1. DB_TRX_ID :标识最近一次对当前数据进行修改的事务ID,这里面官网特意提出delete操作属于updata(注意概念)。

  2. DB_ROLL_PTR :回滚指针,undo中记录多个版本之间的关系,使用指针连接

  3. DB_ROW_ID:如果表里没有主键,也没有非空唯一索引,这个隐藏字段就会作为索引存在。

undo log中存储的数据的结构:

其实就是存储的行的数据:(展示的是undo log 的内容)

借助例子开始展示 现在有一个表数据,里面的数据是 id and name

假设这是第一个事务,所以没有回滚指针..............

假设现在第二个事务将id=1的name设置为李四,展示现在undo log的变化..............

之后可以按着这个思路在尝试自己画一下将李四变为王五,如果画完了,可以在评论区发出来奥!

Read View(快照):

再MVCC下维护了几个变量,依靠变量来验证当前事务是否可以查看undo log 中其他的事务的数据:

  • m_creator_trx_id: 当前事务的TRX_ID,也就是事务ID

    • 人话:当前事务的ID,当前事务要创建这个Read View快照。

  • m_ids: 创建快照时,处于活跃事务的ID集合。

    • 人话:未提交事务的事务的集合。因为事务没提交,所以这里的数据是不可见的。并且活跃事务列表不会记录当前事务。

  • m_low_limit_id: 读取时不应看到任何trx id>=此值的事务。换句话说,这是“高水位线”

    • 人话:当前行数据的事务ID,大于等于m_low_limit_id,数据是不可见的。说白了,当前事务在创建Read View快照时,这个事务他还没开始呢,他的数据必然是不可见的。通过查看m_low_limit_id的赋值,可以得知他是还未被分配的事务的最小事务ID其实就是最大活跃事务 + 1。

  • m_up_limit_id: 读取应该看到所有严格小于(<)此值的trx id。换句话说,这是低水位线”

    • 人话: 他是活跃事务列表中的最小事务ID ,比这个事务ID还要小的值,他事务必然已经提交了,所以如果当前行数据的事务ID 小于 m_up_limit_id,我是可见的。如果活跃事务列表为空,m_up_limit_id = m_low_limit_id。

 

上图中:
首先:我们的当前事务id :12
     活跃事务id: 14 15 
     高水位线:16
     滴水位线:14
     //如果是13  那就是可见,其他的就是不可见.....
    

源码展示判断逻辑:

// id参数,是你想查看的那行数据的事务ID
bool changes_visible(
    trx_id_t        id, const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result)){
    ut_ad(id > 0);
    // m_up_limit_id 是活跃事务的最小id,如果当前行的事务ID,小于m_up_limit_id,说明这个事务必然已经提交了,这个数据是可见的。
    // 如果当前行的事务ID和当前创建ReadView的事务ID相等,说明就是当前事务修改的数据,必然可见。
    if (id < m_up_limit_id || id == m_creator_trx_id) {
        return(true);
    }
​
    check_trx_id_sanity(id, name);
    // 当前行的事务ID,大于了m_low_limit_id,必然不可见。创建Read View的时候,m_low_limit_id这个事务还没有呢。
    if (id >= m_low_limit_id) {
        return(false);
    // 没有活跃事务,并且当前行数据的事务ID,还小于m_low_limit_id,那这个数据必然可见。
    } else if (m_ids.empty()) {
        return(true);
    }
​
    const ids_t::value_type*    p = m_ids.data();
    // 如果上述情况都不满足,无法判断可见还是不可见,此时需要拿着当前行的事务ID,以及活跃事务列表开始判断。
 // 1、如果我发现当前行的事务ID,在活跃事务列表中。此时在Read View来说,这个事务没提交,不可见。
 // 2、如果我发现当前行的事务ID,不在活跃事务列表中,说明创建Read View时候,你就提交了,可见。
    return(!std::binary_search(p, p + m_ids.size(), id));
}

这里面需要注意:

如果是RC隔离级别:事务每次执行select 都会创建快照

对于现在的这个情况,隔离级别是读已提交:

第一次创建快照:
我们的当前事务id :12
     活跃事务id: 14 15 
     高水位线:16
     低水位线:14
     //如果是13  那就是可见,其他的就是不可见.....‘
第二次创建快照:
我们的当前事务id :12
     活跃事务id: 15 
     高水位线:16
     低水位线:15
     //如果是13 ,14 那就是可见,其他的就是不可见.....‘

如果是RR隔离级别:只有第一次执行时会创建快照。

但是如果时RR,他只是会在第一次的查询时创建快照,所以至始至终都是:

我们的当前事务id :12
     活跃事务id: 14 15 
     高水位线:16
     低水位线:14
     //如果是13  那就是可见,其他的就是不可见.....‘