【MySQL】MVCC原理分析 + 源码解读 -- 必须说透


前言

如何控制并发是数据库领域中非常重要的问题之一,MySQL为了解决并发带来的问题,设计了事务隔离机制、锁机制、MVCC机制等等,用一整套机制来解决并发问题,本文主要介绍MySQL5.7版本的MVCC机制。


一、MVCC 介绍

MVCC,全称 Multi-Version Concurrency Control(多版本并发控制)
利用多版本解决的是读写并发冲突,做到读写冲突时,避免加锁,实现非阻塞的读操作,也就是无锁并发控制.

很多数据库也都有各自的实现,像Oracle、PostgreSQL、SQLSerer、MySQL等,但没有统一的标准,所以内部实现也各有差异。


二、MySQL MVCC 介绍

MySQL的InnoDB引擎支持MVCC,工作原理是使用数据在某个时间点的快照来实现。这意味着,无论事务运行多长时间,都可以看到数据的一致视图,也意味着不同的事务可以在同一时间看到同一张表中的不同数据! 上文回顾:MySQL事务隔离机制 – 必须说透

为了更好的理解,我们先了解两个重要概念:当前读和快照读

  • 当前读:官方叫做 Locking Reads(锁定读取),读取数据的最新版本. 常见的 update/insert/delete、还有 select … for update、select … lock in share mode 都是当前读.
    官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
  • 快照读:官方叫做 Consistent Nonlocking Reads(一致性非锁定读取),也就是 MVCC 生成的 ReadView,用于普通的 select 的语句.
    官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html
    这里需要多啰嗦一下consistent read(源码里到处可见):

    在这里插入图片描述


    一种读取操作,使用数据在某个时间点的快照显示查询结果,而不考虑同时运行的其他事务所执行的更改. 如果查询的数据已被另一个事务更改,则会根据undo log的内容重建原始数据. 该技术避免了一些锁定问题,这些问题可以通过强制事务等待其他事务完成来减少并发性.

对于REPEATABLE READ隔离级别,快照基于执行第一次读取操作的时间. 使用READ COMMITTED隔离级别,快照将重置为每次一致读取操作的时间.

一致读取是InnoDB以READ COMMITTED和REPEATABLE READ隔离级别处理SELECT语句的默认模式. 由于一致读取不会对其访问的表设置任何锁,因此在对表执行一致读取时,其他会话可以自由修改这些表.
官方文档:https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_consistent_read


三、MySQL MVCC实现原理+源码分析

MySQL InnoDB引擎实现的MVCC,主要依赖数据行的隐式字段与undo log生成的日志版本链,再结合ReadView可见性判断机制实现.

3.1 隐式字段

在内部,InnoDB向数据库中存储的每一行添加三个字段:

  • DB_TRX_ID :6 byte,插入或更新行的最后一个事务ID. (解读:用于MVCC的ReadView判断事务id)
    此外,删除在内部被视为更新,其中行中的一个特殊位被设置为将其标记为已删除.
  • DB_ROLL_PTR:7 byte,回滚指针. (解读:用于MVCC中指向undo log记录)
    指向已写入回滚段(rollback segment)的一条undo log记录,记录着行(row)更新前的副本.
  • DB_ROW_ID:6 byte,隐藏的自增 ID. (解读:对于MVCC可忽略该字段)
    如果InnoDB自动生成聚集索引,则索引包含这个行ID值. 否则,DB_ROW_ID列不会出现在任何索引中.

大概是这样:

在这里插入图片描述

源码验证

在这里插入图片描述


在这里插入图片描述

参考官方:

在这里插入图片描述

3.2 undo log

undo log,撤销日志。
在事务中,insert/update/delete每一个sql语句的更改都会写入undo log,当事务回滚时,可以利用 undo log 来进行回滚。
undo 日志的存储结构比较复杂,本文不做详细解读. 最小单元的undo log都有nextstart 两个字段,形成一个双向链表,示意图:

在这里插入图片描述

undo log格式

  • insert undo log
    insert undo log是指在insert操作中产生的undo log,仅用于事务回滚. 因为insert操作的记录,只对事务本身可见,对其它事务不可见,所以该日志可以在事务commit后直接删除. 不需要进行purge(后台清除线程)操作. 格式如图7-14所示.
  • update undo log
    update undo log是对delete和update操作产生的的undo log. 该undo log可能需要提供MVCC机制,因此不能在事务commit后就进行删除. 提交时放入undo log链表,等待purge线程(后台清除线程)进行最后的删除. 格式如图7-15所示.

注:图片来源于姜承尧老师的《MySQL技术内幕 InnoDB存储引擎 第2版》
*表示对存储的字段进行了压缩

在这里插入图片描述

  • insert undo log 格式解读
字段 说明
next 2字节,下一个undo log的位置.
type_cmpl 1字节,undo类型. insert un log值固定为11
undo_no undo编号
table_id table.id
lenN + colN 所有列的长度和数据(插入到聚集索引中的)
start 2字节,本条undo log开始的位置.
  • update undo log 格式解读
    next、start、undo_no、table_id、lenN + colN和之前的insert undo log相同,不做重复说明.
字段 说明
DATA_TRX_ID 旧记录的事务id(关键:用于MVCC的ReadView判断事务id
DATA_ROLL_PTR 旧记录的回滚指针(关键:用于MVCC中指向前一个undo log记录
type_cmpl 12 TRX_UNDO_UPD_EXIST_REC 更新non-delete-mark的记录
13 TRX_UNDO_UPD_DEL_REC 将delete的记录标识为not delete
14 TRX_UNDO_UPD_DEL_MARK_REC 将记录标为delete
update_vector 表示update操作导致发生改变的列. (不做详细解读)

undo log源码验证

写insert undo log源码

入口函数:trx_undo_page_report_insert

在这里插入图片描述

写update undo log源码

入口函数:trx_undo_page_report_modify
这里只贴最关键的写trx_id和roll_ptr:

在这里插入图片描述

写undo log源码

不管是insert undo log还是update undo log,
它们只有一处调用:trx_undo_report_row_operation

在这里插入图片描述


最后写完undo log构建当前undo log的roll_ptr

在这里插入图片描述

roll_ptr是如何指向insert undo log的?

入口函数:btr_cur_ins_lock_and_undo

在这里插入图片描述


调用row_upd_index_entry_sys_field设置聚集索引中的trx_id和roll_ptr

在这里插入图片描述

roll_ptr是如何指向update undo log的?

我们已知写undo log的统一入口是 trx_undo_report_row_operation,我们先看调用它的函数: btr_cur_upd_lock_and_undo
从注释可以看出:对于更新,检查锁并追加undo log
另外,如果不是聚集索引就不会写undo log,看红框 We do undo logging only when we update a clustered index record(只有在更新聚集索引记录时,才写undo log)

在这里插入图片描述


咱们再找btr_cur_upd_lock_and_undo的引用,就找到了btr_cur_update_in_place

在这里插入图片描述


row_upd_rec_sys_fields
Updates the trx id and roll ptr field in a clustered index record when a row is updated or marked deleted.
当行被更新或标记为删除时,更新聚集索引记录中的trx-id和roll-ptr字段。

在这里插入图片描述

undo日志版本链演示

为了演示效果,这里我们采用可重复读(RR)级别来演示,演示一下undo日志版本链是如何在读写并发时读到不同版本的,

我们采用6个事务,1个事务insert,2个事务update,另外3个事务读到3个版本的效果.

执行时间轴从上至下预览(下面有分步的undo log和回滚指针指向说明,readview看不懂先忽略,讲完readview再回头来看,
格式:readview[m_ids],m_low_limit_id 其中m_ids最小的是m_up_limit_id):

在这里插入图片描述

  • 事务100 对user表执行 insert + 提交
    说明:这步就是为了构建原始记录
begin;
insert into user(id,name) values(1,'张三');
commit;

在这里插入图片描述

  • 然后,事务101 对user表执行 update + 提交
begin;
update user set name = '李四' where id = 1;
commit;

在这里插入图片描述

  • 同时,事务102事务101提交前,查询了该记录:
    说明:有事务使用的undo log,purge线程不会清除这条记录
begin;
-- 读到的name为张三
select * from user where id = 1; 
  • 同时,事务103 对user表执行 update 未提交
begin;
update user set name = '王五' where id = 1;

在这里插入图片描述

  • 然后,事务102 再次读取: 在RR级别,后面所做的更改依然不可见:
begin;
-- 读到的name为张三
select * from user where id = 1;

-- 读到的name仍为张三
select * from user where id = 1;
  • 同时,事务104 执行了查询,读取到了事务101提交的“李四”(因为事务103尚未提交):
begin;
-- 读到的name为李四
select * from user where id = 1;
commit;
  • 然后,事务103 提交,事务105来了,它读取到的却是“王五”. 3个事务看到了3个版本,版本链就是这样的,还可能会更多版本…

purge线程

上述提到的 「purge 线程],是一个周期运行的垃圾收集线程, 对于没有事务引用的undo log进行清除,上面演示里由于都有事务读取,所以purge线程不会清除. 但当purge线程发现undo log没有事务引用时将自动清除. 另外,从上面我们得知,清除的是update undo log,因为insert undo log在事务完成时直接删除.

  • innodb 会将所有需要清理的任务添加到 purge 队列中,可以通过 innodb_max_purge_lag 配置项设定 purge 队列的大小
  • 通过show variables like ‘%purge%’ 查看purge参数

本文由 [天罡gg] 首发于csdn,转载请注明出处:https://blog.csdn.net/scm_2008/article/details/127985117

3.3、ReadView

一听到view,大家可能会联想到数据库视图,然后可能会误解为把数据库当前所有表都通过视图快照起来,其实不是,那样的话得多占空间,性能得多差啊。其实生成的ReadView里面主要保存的是用于比较可见性的事务id等。

ReadView保存在事务对象trx_t中

TrxVersion

Readview核心字段

在这里插入图片描述


先说结论,下面再来验证

字段 说明 可见性说明
m_low_limit_id 尚未分配的最小事务id >=它的,都不可见
m_up_limit_id 最小活动未提交事务id <它的,都可见
m_creator_trx_id 创建readview的事务id =它的,都可见
m_ids 创建readview所有活动未提交的事务ids 在m_ids里面不可见,否则可见

核心字段在prepare和complete里赋值

从下面的源码里,可以验证上面4个字段的说明是准确的.
trx_sys->max_trx_id 的注释说明是:The smallest number not yet assigned as a transaction id or transaction number.

在这里插入图片描述

如何判断记录的可见性?

入口函数:changes_visible
从下面的源码里,可以验证上面4个字段的可见性说明是准确的.

在这里插入图片描述


在这里插入图片描述

搞懂了可见性,我们再看如何通过ReadView实现快照读(consistent read)?

  • 先判断聚集索引中的记录是否可见
    lock_clust_rec_cons_read_sees
    检查是否在一致读取中看到记录。
    如果可以看到,返回true;如果应检索记录的早期版本,则返回false

    在这里插入图片描述

  • 不可见时,再通过回滚指针找到可见的版本记录
    在不同的调用链路上会调下面这两个函数(注释都是一样的):
    row_sel_build_prev_vers_for_mysql() // 为“一致读取”生成聚集索引记录的早期版本
    row_sel_build_prev_vers() // 为“一致读取”生成聚集索引记录的早期版本
    然后这两个函数内部都会调
    row_vers_build_for_consistent_read()

    在这里插入图片描述

最后我们再来看下ReadView的生命周期(何时分配,保时关闭)?

  • 可重复读(RR)级别
    入口函数:innobase_start_trx_and_assign_read_view
    开始事务并分配一致性读的快照readview(如果还没有)

    在这里插入图片描述


    看看内部调的trx_assign_read_view,命名非常准确!

    在这里插入图片描述


    view_open了,里面调prepare和complete生成ReadView为字段赋值了

    在这里插入图片描述

  • 读已提交(RC)级别
    对于读已提交(RC)级别,每次select都会重新分配readview,因为每次SQL语句结束后,会关闭readview.
    分配在 row_sel_step

    在这里插入图片描述


    关闭在external_lock

    在这里插入图片描述


总结

通过本文我们已经详细说明了:

  1. MVCC介绍
  2. MySQL MVCC 介绍
  3. MySQL MVCC实现原理+源码分析(这里面最关键的是undo日志版本链和ReadView的可见性判断)

如果感觉不错,请关注我分享更多干货:天罡gg https://blog.csdn.net/scm_2008
大家的「关注 + 点赞 + 收藏」就是我创作的最大动力!


参考:
图文带你彻底弄懂MySQL事务原子性之UndoLog
MySQL · 引擎特性 · InnoDB undo log 漫游
MySQL 8.0 MVCC 核心原理解析(核心源码)
InnoDB MVCC实现原理及源码解析
【MySQL笔记】正确的理解MySQL的MVCC及实现原理
《MySQL技术内幕 InnoDB存储引擎 第2版》
官网14.3 InnoDB Multi-Versioning
官网14.6.3.4 Undo Tablespaces
官网14.6.7 Undo Logs
Mysql 核心日志(redolog、undolog、binlog)
图文结合带你搞定MySQL日志之Undo log(回滚日志)
MVCC详解,深入浅出简单易懂
详解 MySQL 的 undo log
庖丁解InnoDB之UNDO LOG

大家的「关注 + 点赞 + 收藏」就是我创作的最大动力!


原文地址:https://blog.csdn.net/scm_2008

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在正式开始之前,我们先来看下 MySQL 服务器的配置和版本号信息,如下图所示: “兵马未动粮草先行”,看完了相关的配置之后,我们先来创建一张测试表和一些测试数据。 -- 如果存在 person 表先删除 DROP TABLE IF EXISTS person; -- 创建 person 表,其中
&gt; [合辑地址:MySQL全面瓦解](https://www.cnblogs.com/wzh2010/category/1859594.html &quot;合辑地址:MySQL全面瓦解&quot;) # 1 为什么需要数据库备份 - 灾难恢复:当发生数据灾难的时候,需要对损坏的数据进行恢复和
物理服务机的CPU、内存、存储设备、连接数等资源有限,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈。为了解决这个问题,行业先驱门充分发扬了分而治之的思想,对大库表进行分割,&#xA;然后实施更好的控制和管理,同时使用多台机器的CPU、内存、存储,提供更好的性能。而分治有两种实现方式:垂直拆
1 回顾 上一节我们详细讲解了如何对数据库进行分区操作,包括了 垂直拆分(Scale Up 纵向扩展)和&#160;水平拆分(Scale Out 横向扩展) ,同时简要整理了水平分区的几种策略,现在来回顾一下。 2 水平分区的5种策略 2.1 Hash(哈希) 这种策略是通过对表的一个或多个列的Ha
navicat查看某个表的所有字段的详细信息 navicat设计表只能一次查看一个字段的备注信息,那怎么才能做到一次性查询表的信息呢?SELECT COLUMN_NAME,COLUMN_COMMENT,COLUMN_TYPE,COLUMN_KEY FROM information_schema.CO
文章浏览阅读4.3k次。转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52768613前言:数据库每天的数据不断增多,自动删除机制总体风险太大,想保留更多历史性的数据供查询,于是从小的hbase换到大的hbase上,势在必行。今天记录下这次数据仓库迁移。看下Agenda:彻底卸载MySQL安装MySQL_linux服务器进行数据迁移
文章浏览阅读488次。恢复步骤概要备份frm、ibd文件如果mysql版本发生变化,安装回原本的mysql版本创建和原本库名一致新库,字符集都要保持一样通过frm获取到原先的表结构,通过的得到的表结构创建一个和原先结构一样的空表。使用“ALTER TABLE DISCARD TABLESPACE;”命令卸载掉表空间将原先的ibd拷贝到mysql的仓库下添加用户权限 “chown . .ibd”,如果是操作和mysql的使用权限一致可以跳过通过“ALTER TABLE IMPORT TABLESPACE;”命令恢_alter table discard tablespace
文章浏览阅读225次。当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化:单表优化除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。而事实上很多时候MySQL单表的性能依然有不少优化空间,甚至能正常支撑千万级以上的数据量:字段尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNEDVARCHAR的长度只分配_开发项目 浏览记录表 过大怎么办
文章浏览阅读1.5k次。Mysql创建、删除用户MySql中添加用户,新建数据库,用户授权,删除用户,修改密码(注意每行后边都跟个;表示一个命令语句结束):1.新建用户登录MYSQL:@>mysql -u root -p@>密码创建用户:mysql> insert into mysql.user(Host,User,Password) values("localhost_删除mysql用户组
MySQL是一种开源的关系型数据库管理系统,被广泛应用于各类应用程序的开发中。对于MySQL中的字段,我们需要进行数据类型以及默认值的设置,这对于数据的存储和使用至关重要。其中,有一个非常重要的概念就是MySQL字段默认字符串。 CREATE TABLE `my_...
MySQL是一个流行的开源关系型数据库管理系统,广泛应用于Web应用程序开发、数据存储和管理。在使用MySQL时,正确设置字符集非常重要,以确保数据的正确性和可靠性。 在MySQL中,字符集表示为一系列字符和字母的集合。MySQL支持多种字符集,包括ASCII、UTF...
MySQL存储函数 n以内偶数 MySQL存储函数能够帮助用户简化操作,提高效率,常常被用于计算和处理数据。下面我们就来了解一下如何使用MySQL存储函数计算n以内的偶数。 定义存储函数 首先,我们需要定义一个MySQL存储函数,以计算n以内的偶数。下...
MySQL是一个流行的关系型数据库管理系统,基于客户机-服务器模式,可在各种操作系统上运行。 MySQL支持多种字符集,不同的字符集包括不同的字符,如字母、数字、符号等,并提供不同的排序规则,以满足不同语言环境的需求。 //查看MySQL支持的字符集与校对规...
在MySQL数据库中,我们有时需要对特定的字符串进行截取并进行分组统计。这种操作对于数据分析和报表制作有着重要的应用。下面我们将讲解一些基本的字符串截取和分组统计的方法。 首先,我们可以使用substring函数对字段中的字符串进行截取。假设我们有一张表stude...
MySQL提供了多种字符串的查找函数。下面我们就一一介绍。 1. LIKE函数 SELECT * FROM mytable WHERE mycolumn LIKE 'apple%'; 其中"apple%"表示以apple开头的字符串,%表示任意多个字符...
MySQL 是一种关系型数据库管理系统,广泛应用于各种不同规模和类型的应用程序中。在 MySQL 中,处理字符串数据是很常见的任务。有时候,我们需要在字符串的开头添加一定数量的 0 ,以达到一定的位数。比如,我们可能需要将一个数字转换为 4 位或 5 位的字符串,不足的...
MySQL是一种流行的关系型数据库管理系统,支持多种数据类型。以下是MySQL所支持的数据类型: 1. 数值型数据类型: - TINYINT 保存-128到127范围内的整数 - SMALLINT 保存-32768到32767范围内的整数 - MEDIU...
MySQL中存储Emoji表情字段类型 在现代互联网生态中,表情符号已经成为人们展示情感和思想的重要方式之一,因此将表情符号存储到数据库中是一个经常出现的问题。MySQL作为最流行的开源关系型数据库管理系统之一,也需要能够存储和管理这些表情符号的字段类型。 UT...
MySQL是一种关系型数据库管理系统。在MySQL数据库中,有多种不同的数据类型。而其中,最常见的数据类型之一就是字符串类型。在MySQL中,字符串类型的数据通常会被存储为TEXT或VARCHAR类型。 首先,让我们来看一下VARCHAR类型。VARCHAR是My...
MySQL字符串取整知识详解 MySQL是一种开源的关系型数据库管理系统,广泛应用于各个领域。在使用MySQL过程当中,我们经常需要对数据进行取整操作。本文将介绍如何使用MySQL字符串取整来处理数据取整问题。 什么是MySQL字符串取整? MySQL...