mysql系列9—mysql的MVCC机制

news/2025/2/24 11:28:29

背景

mysql提供了读未提交、读已提交、可重复读、串行化四种隔离级别,默认的隔离界别为可重复读。其中,不可重复度场景下,每次直接读取最新记录(即使事务未提交);串行化对于所有的读写都加锁,因此,对二者不做过多介绍,MVCC只适用于可重复度和读已提交。
mysql的并发事务中存在三种冲突:读-读、读-写、写-写。其中读-读没有冲突,可以同时进行;写-写通过数据库锁实现冲突分离;MVCC解决的是读-写冲突,并尽可能提高数据库性能。

MVCC的实现由UNDO日志、版本链、READVIEW三部分组成,因此本文先对这三部分进行介绍,然后再结合案例介绍MVCC机制;最后介绍MVCC遗留的幻读问题。
本文中较大篇幅围绕案例进行,案例使用的表如下:

CREATE TABLE `t_student` (
	`id` INT(10) NOT NULL COMMENT '学号,唯一ID',
	`name` VARCHAR(50) NOT NULL COMMENT '姓名',
	PRIMARY KEY (`id`) USING BTREE
)
ENGINE=InnoDB
;

1.当前读和快照读

当前读也叫做锁定读,数据读取时需要加锁(行锁和间隙锁),以保证读取的数据不会被其他事务修改,从而读取的是当前最新的数据。
mysql中以下两种SELECT查询语句会触发当前读:

select ... for update

select ... lock in share mode

当前读会阻塞其他事务对行记录的写操作,一般用于金融交易等对一致性要求较高的场景。
除了上述两种SELECT查询之外的其他查询SQL都是快照读。
mysql的默认隔离级别为可重复读,在这种隔离界别下,第一次查询时生成一个数据库快照,后续查询时读取的是同一份快照。

2.版本链

创建三个事务,分别对t_student表按如下时间线顺序和操作进行修改:

time事务100事务200事务300
1START TRANSACTION;
2INSERT INTO t_student (id, name) VALUES (1, ‘sy’);
3COMMIT;
4START TRANSACTION;
5START TRANSACTION;
6UPDATE t_student SET NAME = ‘sy201’ WHERE id = 1;
7UPDATE t_student SET NAME = ‘sy202’ WHERE id = 1;
8COMMIT;
9UPDATE t_student SET NAME = ‘sy301’ WHERE id = 1;
10UPDATE t_student SET NAME = ‘sy302’ WHERE id = 1;
11COMMIT;

在UNDO文章中已经介绍过版本链的概念,案例对应的版本链结构(近似表示)如下图所示:
请添加图片描述

mysql可以根据undo日志计算出上一步的记录值,因此上图可以形象地表示为:
请添加图片描述
简单回顾一下图中涉及两个重要的隐藏列: db_trx_id表示当前记录由哪个事务创建或者最后改动; db_trx_ptx指向undo日志。实际上,每条被修改的记录都通过db_trx_ptx指向上一步的Undo日志,undo日志中又包含了上上步Undo日志指针…… 由此形成了该记录对应的版本链。

3.ReadView和可见性原则

ReadView结合可见性原则可以实现读已提交,解决并发脏读问题。

ReadView:

ReadView由四个属性组成:
[1] m_ids: 当前活跃的事务ID;
[2] min_trx_id: m_ids中的最小值;
[3] max_trx_id: 下一个待分配的事务ID;
[4] creator_trx_id:当前事务ID;

可见性原则:

由于事务ID是依次递增的,即越早提交的事务,事务ID越小。在前文提到过,事务ID只有在执行修改时才会分配,而不是事务启动时分配。如一个事务全局只会读数据,则该事物不会分配事务ID。
基于上述两点,以下可见性原则保证了读取的数据均为已提交数据。
请添加图片描述
Note: 版本链上的事务ID使用trx_id表示。
[1] trx_id 等于creator_trx_id,表示当前事务在访问自己修改过的记录,可见;
[2] trx_id 小于min_trx_id, 表示记录对应的事务已经被提交,可见;
[3] trx_id 大于max_trx_id, 表示记录对应的事务未被提交,不可见;
[4] 其他情况(处于min_tx_id和max_trx_id之间),如果事务已提交(trx_id 不在m_ids集合中)——可见,事务活跃(trx_id 在m_ids集合中),则不可见。

读已提交和可重复度通过创建ReadView的策略不同进行区分: ReadView本身保证了读取的记录已经被提交,因此读已提交隔离级别下,每次读取都重新创建一个ReadView; 可重复读只在第一次查询时建立ReadView, 后续查询都基于同一个ReadView进行,保证了每次读取的数据相同。

4.MVCC

查询时事务将根据ReadView信息沿着版本链的顺序进行,直到查询满足可见性原则的记录或者返回空。
结合案例进行理解。

time事务100事务200事务300
1START TRANSACTION;
2INSERT INTO t_student (id, name) VALUES (1, ‘sy’);
3COMMIT;
4START TRANSACTION;
5START TRANSACTION;
6UPDATE t_student SET NAME = ‘sy201’ WHERE id = 1;… update other line …
7UPDATE t_student SET NAME = ‘sy202’ WHERE id = 1;
8SELECT NAME FROM t_student WHERE id = 1;
9SELECT NAME FROM t_student WHERE id = 1;
10COMMIT;
11COMMIT;

在time=NO.8和NO.9时刻,id=1记录的版本链如下所示:
请添加图片描述
case 1: 站在事务200的角度,time=NO.8时刻第一次执行版本查询,创建ReadView(用ReadView200表示):
[1] m_ids: 200;
[2] min_trx_id: 200;
[3] max_trx_id: 301;
[4] creator_trx_id: 200;
使用ReadView200与第一条记录按照可见性原则进行匹配:
记录的db_trx_id等于creator_trx_id,表明该记录是当前事务创建的,因此可见。
返回的结果是sy202.

case 2: 站在事务300的角度,time=NO.9时刻第一次执行版本查询,创建ReadView(用ReadView300表示):
[1] m_ids: {200,300};
[2] min_trx_id: 200;
[3] max_trx_id: 301;
[4] creator_trx_id: 300;

使用ReadView300与第一条记录按照可见性原则进行匹配:
记录的db_trx_id(200)不等于creator_trx_id(300), 且db_trx_id(200)事务还处于活跃状态,不可见;
沿着版本链读取下一条记录: 记录的db_trx_id(200),仍不可见;
沿着版本链继续读取下一条记录: db_trx_id(100)小于min_trx_id,表示事务已提交,可见,返回sy.

特别注意:ReadView是在第一次查询时创建, 而不是开启事务时创建

对上述案例进行简单变更(事务3的查询时机修改一下):

time事务100事务200事务300
1START TRANSACTION;
2INSERT INTO t_student (id, name) VALUES (1, ‘sy’);
3COMMIT;
4START TRANSACTION;
5START TRANSACTION;
6UPDATE t_student SET NAME = ‘sy201’ WHERE id = 1;… update other line …
7UPDATE t_student SET NAME = ‘sy202’ WHERE id = 1;
8SELECT NAME FROM t_student WHERE id = 1;
9COMMIT;
10SELECT NAME FROM t_student WHERE id = 1;
11COMMIT;

case 2: 站在事务300的角度,time=NO.10时刻第一次执行版本查询,创建ReadView(用ReadView300表示):
[1] m_ids: {300};
[2] min_trx_id: 300;
[3] max_trx_id: 301;
[4] creator_trx_id: 300;
此时,根据可见性规则,事务200的修改已对事务300可见,即查询的结果为sy202.

5.幻读问题

幻读的概念: 一个事务在读取某个范围内的记录后,另一个并发事务插入了新的记录到这个范围内,导致第一个事务再次读取该范围时,出现了之前未读到的记录,就像产生了“幻影”一样。

事务中完全使用当前读可以避免幻读问题,当前读通过加锁实现: 行锁保证了当前记录不会被修改,间隙锁保证了范围查询时,范围内不会插入新记录。
mysql虽然通过MVCC解决了大部分幻读问题,但特殊场景下仍存在幻读问题:一个事务A读取某个范围内的记录后,另一个并发事务B插入了新的记录到这个范围内并提交; 事务A修改这条记录,事务A再次读取相同范围时,会多查出这条记录。以下通过案例进行细致介绍。

t_student表中初始状态只有1条记录:

mysql> select * from t_student;
+----+-------+
| id | name  |
+----+-------+
|  1 | sy    |
+----+-------+

创建两个事务,分别对t_student表按如下时间线顺序和操作进行修改:

time事务100事务200
1START TRANSACTION;
2SELECT * FROM t_student WHERE id < 10;
3START TRANSACTION;
4INSERT INTO t_student (id, name) VALUES (2, ‘mf’);
5INSERT INTO t_student (id, name) VALUES (3, ‘lj’);
6COMMIT;
7SELECT * FROM t_student WHERE id < 10;
8UPDATE t_student SET NAME = ‘mf100’ WHERE id = 2;
9SELECT * FROM t_student WHERE id < 10;

对应的执行结果如下所示:
请添加图片描述
NO.7范围查询与NO.2的查询结果一致,此时没有幻读问题;NO.9的查询与NO.1的不一致,多查出了其他事务插入的数据,出现幻读。

原因分析:
mysql本质上不能保证两个事务完全彻底的独立,仅通过MVCC机制实现了读的隔离, 在一定程度上保证了读写分离和并发安全性; 上图NO.8的修改操作显示有一条记录被改动,对比NO.7的查询可知:事务100无法读取,但可以修改已被提交的记录。
此时,id=2的记录对应的db_trx_id记录的是最后修改的事务ID,即100;事务100再次查询该范围时,根据ReadView和可见性原则,就可以查询到这条幻影记录了。


http://www.niftyadmin.cn/n/5864267.html

相关文章

hive开窗函数边界值ROWS BETWEEN 和 RANGE BETWEEN区别

目录 一、概念 1.rows between ... and ... 2.range between ... and ... 二、语法 1.关键词含义 一、概念 1.rows between ... and ... rows&#xff1a;指以行号来决定frame的范围&#xff0c;是物理意义上的行。 2.range between ... and ... range&#xff1a;指以当…

DeepSeek从入门到精通

1_DeepSeek从入门到精通 (1).pdf官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘123云盘为您提供1_DeepSeek从入门到精通 (1).pdf最新版正式版官方版绿色版下载,1_DeepSeek从入门到精通 (1).pdf安卓版手机版apk免费下载安装到手机,支持电脑端一键快捷安装https://www.123…

生活教练项目_Trae

XMXALifeCoach - 个人成长辅导网站 网址: https://github.com/zhangxiaomeng1/XMXALifeCoach 项目简介 这是一个基于DeepSeek R1 API开发的个人成长辅导网站。通过与AI进行对话&#xff0c;用户可以获得个性化的建议和指导&#xff0c;帮助个人成长。 技术架构 前端&#xff1a…

使用MyCAT实现分布式MySQL双主架构

Mycat是一个开源的分布式数据库中间件&#xff0c;主要用于提供数据库的分库分表、读写分离、负载均衡等功能。以下是Mycat的几个主要作用&#xff1a; 分库分表&#xff1a;Mycat可以将单个数据库拆分成多个小数据库&#xff0c;并将数据分布在不同的节点上&#xff0c;以提高…

《Mycat核心技术》第17章:实现MySQL的读写分离

作者&#xff1a;冰河 星球&#xff1a;http://m6z.cn/6aeFbs 博客&#xff1a;https://binghe.gitcode.host 文章汇总&#xff1a;https://binghe.gitcode.host/md/all/all.html 星球项目地址&#xff1a;https://binghe.gitcode.host/md/zsxq/introduce.html 沉淀&#xff0c…

全星FMEA软件系统:赋能企业研发管理,打造可靠产品

全星FMEA软件系统&#xff1a;赋能企业研发管理&#xff0c;打造可靠产品 在当今竞争激烈的市场环境中&#xff0c;产品质量和可靠性已成为企业立足之本。然而&#xff0c;传统的FMEA分析方式往往耗时耗力&#xff0c;效率低下&#xff0c;难以满足企业快速迭代和降本增效的需求…

设计模式-组合模式、模板模式

组合模式 定义 将对象组合成树形结构以表示"部分-整体"的层次结构&#xff0c;组合模式使得用户对单个对象和组合对象的使用具有一致性&#xff1b; 组合模式实现的最关键的地方是-简单对象和复合对象必须实现相同的接口。这就是组合模式能够将组合对象和简单对象进…

Qt::MouseButtons解析

一 问题 今天想自定定义一个QMouseEvent变量,变量的的初始化参数有Qt::MouseButtons,这是个啥?查看类型为QFlags<Qt::MouseButton>。 二 Qt::MouseButton Qt::MouseButton 是 Qt 框架中定义的一个枚举类型(enum),用于表示鼠标事件中的物理按钮。它是 Qt 事件处理…