怎么建设像天猫的网站,免费问题咨询,网站建设活动海报,无法打开wordpress网页文章目录 1. 什么是MVCC2. MVCC核心组成#xff08;三大件#xff09;2.1 MVCC为什么需要三大件 3. 隐藏字段4. undo log4.1 模拟版本链数据形成过程 5. Read View5.1 m_ids5.2 m_creator_trx_id5.3 m_low_limit_id5.4 m_up_limit_id5.5 可见性分析算法 6. MVCC流程模拟6.1 R… 文章目录 1. 什么是MVCC2. MVCC核心组成三大件2.1 MVCC为什么需要三大件 3. 隐藏字段4. undo log4.1 模拟版本链数据形成过程 5. Read View5.1 m_ids5.2 m_creator_trx_id5.3 m_low_limit_id5.4 m_up_limit_id5.5 可见性分析算法 6. MVCC流程模拟6.1 RC隔离级别6.2 RR隔离级别 以下讨论均建立在innodb存储引擎上ReadView部分官方文档基本都是一笔带过为了保证文章的正确性这部分涉及到大量的源码分析 1. 什么是MVCC
一种数据库中用于处理并发读写事务的技术。它通过维护数据的不同版本来实现对同一数据项的并发访问并且在保证事务隔离性的同时允许读操作无需加锁就能获取一致性的数据视图。
2. MVCC核心组成三大件 MVCC的实现离不开上图所示的三大组件
数据行的隐藏字段undo logread view
2.1 MVCC为什么需要三大件
我们按照逻辑来简单梳理下为什么MVCC需要这三个部分。
首先在并发情况下MVCC需要维护数据行的多个版本那多个版本信息存储在哪呢我们可以率先排除数据库表数据库表是存储索引已提交数据的再多维护一份历史数据.idb文件要骂人了.idb是innodb下存储表数据的文件后缀。为了减轻.idb文件的压力我们不妨在多创建一个文件用于存储数据行的多个版本而这便是undo log
我们知道多版本数据存储在undo log中和数据表中的数据是分开存储的那么每行数据如何找到它的历史版本呢MySQL将数据写入数据库文件时为多创建几个字段列其中一个字段就是专门存储指针用于指向历史版本的指针。这也就是隐藏字段存在的原因
现在MySQL的行数据能够关联到历史数据那每个事务在查询时获取哪个版本的数据呢read view就是提供了数据版本确定的依据。再查询数据时MySQL会生成read viewread view提供了选择哪个版本数据的依据
3. 隐藏字段 DB_TRX_ID 这是一个6字节长的字段用来记录最后一次插入或更新该行记录的事务标识符。当某一行被删除时内部会将其视为一种特殊的更新操作在行的一个特定位上设置标记以表示其已被删除。 DB_ROLL_PTR 也称为回滚指针是一个7字节长的字段。它指向回滚段中的一个undo日志记录。如果该行被更新则undo日志记录包含重建更新前行内容所需的信息。当需要进行事务回滚或者为了实现一致性读时可以通过回滚指针找到并应用相应的undo日志。 DB_ROW_ID 这是一个6字节长的行ID字段每当新行插入时它的值就会单调递增。如果InnoDB自动生成聚簇索引那么这个聚集索引中就会包含行ID值。否则DB_ROW_ID列不会出现在任何索引中。行ID可以帮助系统在没有用户定义唯一键的情况下为行提供一个唯一的标识并且在某些查询和排序场景下也能发挥作用。
4. undo log
undo log是回滚日志其中记录包含了关于如何撤销事务对聚集索引记录所做的最新更改的信息。事务在对数据操作前undolog会记录数据原本的数值方便事务回滚回复到上一个版本的数据
undo log日志会准备两份一份用于记录insert操作另一份记录update 或者 delete操作
在不指定的情况下undo log默认会在data文件夹下创建并命名为undo_001undo_002 tip: 当insert时undo log日志只会在回滚时需要事务提交后可被立即删除当update、delete时undo log日志不仅在回滚时需要、快照读时也需要不会被立刻删除 4.1 模拟版本链数据形成过程
我们配合隐藏字段通过undo log来模拟版本链形成的过程
step 1 一开始事务1插入一条id 4age 18的数据数据隐藏字段DB_TRX_ID 1当前事务id 1DB_ROLL_PTR null没有历史版本。此时事务2、3、4、5全部开启事务begin
step 2 事务2执行update操作将id 1数据行的age字段设置为20。MySQL更新逻辑是先更新buffer pool中缓存的数据页形成脏页。当事务提交后将数据刷到磁盘中。
此时MySQL会将当前记录存储到undo log中用于数据回滚其地址为0x001
step 3 事务2执行commit操作buffer pool中的脏页被持久化到磁盘中。数据被永久更改。一方面age被设置为20另一方面DB_TRX_ID(最新修改的事务id)设置为2DB_ROLL_PTR指向上一个数据版本也就是0x001位置上的数据形成版本链
step 4 事务3执行update操作修改缓存中的数据同时将当前记录写入到undo log中其地址为0x002
step 5 事务3提交事务数据被持久化到磁盘。当前记录更改age设置为70DB_TRX_ID设置为3DB_ROLL_PTR指向上一个版本的数据——0x002追加版本链信息
step 6 事务4执行update逻辑修改内存中的数据并将当前记录写入undo log为其分配地址0x003
step 7 事务4提交事务数据被持久化。age被修改为50DB_TRX_ID设置为当前事务id也就是4DB_ROLL_PTR指向上个版本的数据——0x003。至此完整的版本连形成。 ps: 终于写完这部分了光这个图就累死我了 5. Read View
readView视图存在如下4个字段
m_ids存储当前快照生成时所有活跃事务idm_low_limit_idmax_trx_id预分配事务id等于当前最大事务id 1事务是自增的m_up_limit_idmin_trx_id最小活跃事务idm_creator_trx_idReadView创建者的id 源码分析
5.1 m_ids /** Set of RW transactions that was active when this snapshotwas taken */ids_t m_ids;m_ids类型是ids_tids_t是ReadView的一个内部类 ids_t类中维护如下数据 /** Memory for the array */value_type* m_ptr;/** Number of active elements in the array */ulint m_size;/** Size of m_ptr in elements */ulint m_reserved;friend class ReadView;m_ptr指向的是一块连续的内存空间也就是数组。数组中每个元素的类型是value_type追溯源码后发现value_type是无符号64位整数。为了方便后文叙述我们把m_ptr当成数组 tip: ids_t维护m_ptr的增删改等方法使得数组能够自动扩容 m_size维护数组的大小ulint也是无符号64位整数 m_reserved维护m_ptr中元素个数
现在我们可以初步总结m_ids的作用。m_ids中维护了一个数组而该数组保存了ReadView生成时活跃的事务id。我们可以把m_ids看作成Java里的List集合
此外还得强调一点m_ids中存储id是按照递增顺序
在阅读源码时笔者注意到ids_t类中声明的insert方法 注释中的preserving the order表明ids_t这个类维护的数据是有序的但因为ids_t这个类是申明在read0types.h这个头文件中insert方法没给出实现因此我们需要找到insert方法实现的类阅读insert的具体代码这样我们才能得知ids_t维护的数据是以什么顺序存储的
定位到read0read.cc我们找到insert的实现 笔者已经对部分代码做出中文注解故额外的逻辑不再赘述感兴趣的读者可以自行阅读。
我们主要关注定位的逻辑 value_type* ub std::upper_bound(data(), end, value);if (ub end) {push_back(value); } 源码中调用std标准库提供的upper_bound函数该函数底层是二分查找目的是在给定地址范围内找到大于目标数据的地址并返回。如果找不到则返回末尾迭代器指向数组的最后一个元素的下一个位置
在insert方法中upper_bound的目的是在m_ptr指向的数组中找到一个最小大于value的元素并返回它的地址。在源码中value_type* ub就是指向value应该插入的地址。
如果ub指向end也就是数组末尾这意味着m_ptr指向的数组中不存在大于value的元素需要将value插入的数组末尾。这说明m_ptr数组中最大的元素存储在末尾这就表明了ids_t维护的数组是升序
5.2 m_creator_trx_id
这个字段没啥好说的谁创建ReadView就赋值谁的id
5.3 m_low_limit_id /** The read should not see any transaction with trx id thisvalue. In other words, this is the high water mark. */trx_id_t m_low_limit_id;我们注意到注释中并未直接解释m_low_limit_id是什么含义我们只知道如当事务id如果 readView.m_low_limit_id事务是不可见的。
m_low_limit_id就是高水位事务id 大于这个值那么事务就是不可见的。但它为什么是预分配事务id呢
我们定位到m_low_limit_id赋值的地方 由m_low_limit_id trx_sys-max_trx_id这行代码可知m_low_limit_id的值是事务系统中的max_trx_id
我们继续查看max_trx_id相关代码
发现如下信息 max_trx_id 是还未被分配的最小事务id也就是预分配id同样也可以被等价理解为最大事务id因为事务id是自增的还未被分配的自然是已有的事务id的最大值
因此我们得出结论m_low_limit_id基本等价于max_trx_id是预分配的id等于已存在最大事务id 1
5.4 m_up_limit_id
由源码注释可知m_up_limit_id是低水位事务id小于这个值的都是可见的 但它为什么是当前活跃事务的最小事务id呢
我们定位到赋值时的源码 由注释可知m_up_limit_id存储的是最小活跃id。其实看到注释就已经很明白了但笔者想要结合前文说到m_ids的一个特点来讲解我们如何得到这个结论。
观察这段代码 m_up_limit_id !m_ids.empty() ? m_ids.front() : m_low_limit_id;我们发现在正常情况下m_up_limit_id取的是m_ids.front()值
而m_ids.front()弹出的是数组第一个元素m_ids维护的数据是升序因此第一个元素就是当前活跃的最小id tip: m_ids数组中的id是升序这个结论在m_ids这个章节已经详细介绍这里不在赘述 5.5 可见性分析算法 可见性分析算法能够判断undo log版本链上的版本是否可见。MySQL需要将版本对应的事务id和ReadView中的数据进行判断。
判断规则总结如下
id m_creator_trx_id 可以访问该版本。因为数据就是当前事务更改的id m_up_limit_id min_trx_id可以访问该版本此时数据已经commit了id m_low_limit_idmax_trx_id不能访问该版本当前事务是在ReadView创建后开启的id不在m_ids中可以访问该版本因为m_ids存储活跃id不活跃说明已经commit了
6. MVCC流程模拟
至此我们介绍完MVCC所有组件现在我们复用模拟版本连数据形成章节出现的案例结合ReadView讲解MVCC是如何工作的 tip: 当事务的隔离级别是READ UNCOMMITEDSERIALIZABLE时MVCC基本不起作用 READ UNCOMMITED读未提交。都读未提交了并发管理个锤子。全部脏读爱咋地咋地SERIALIZABLE串行化。直接加锁锁定数据只允许有一个事务控制被修改的数据其他事务都得等着。这有个锤子的并发 6.1 RC隔离级别
RC隔离级别每次select都会创建新的ReadView
我们以事务5第一次select时的情况分析 此时我们将ReadView和undo log版本链上的数据进行比对调用可见性分析算法分析当前undo log数据链上的版本哪个是可见的 首先是版本链上最新的数据也就是地址在0x002的数据。这个数据版本是被trx_id 2的事务创建。trx_id 2和ReadView进行分析我们发现 id min_trx_id因此当前数据可以被访问。
检查原图也好理解在事务5 select之前数据就已经被事务2提交了。因此本次查询到的数据是 id 1, age 20 现在我们以事务5第二次select为例进行分析 我们将undo log版本链上的数据和ReadView中的数据进行比较 我们发现0x003上的数据trx_id 3小于ReadView的min_trx_id。这也就说明0x003版本的数据在ReadView生成之前被创建可以被访问。 检查原图逻辑无误。因此本次查询到的数据是 id 1, age 70
6.2 RR隔离级别
在RR隔离级别下ReadView只会以第一次创建的ReadView为准。因此事务5第一次查询ReadView视图和RC级别下的第一次查询没有区别。查询得到的数据是id 1age 20
但RR级别下第二次的ReadView依然不变。此时ReadView和undo log情况如下图所示 首先判断0x003的数据trx_id 3不满足ReadView中的任何可见性判断规则因此判断0x002版本的数据。
0x002版本的数据trx_id 2小于ReadView的min_trx_id因此可见所以此时查询到的数据依然是id 1, age 20
这也就实现了可重复读隔离级别