• 详解 undoLog 在 MySQL 多版本并发控制 MVCC 中的运用

详解 undoLog 在 MySQL 多版本并发控制 MVCC 中的运用

2025-05-19 08:37:03 栏目:宝塔面板 19 阅读

在当今的数据库世界中,MySQL 以其强大的功能和广泛的应用备受瞩目。而其中的 MVCC(多版本并发控制)和事务隔离级别更是关键且核心的概念,它们犹如数据库运行的精密齿轮,协同作用确保着数据的完整性、一致性和高效的并发处理。

一、详解事务的基本概念

1. 什么是事务

现在我们开发的一个功能需要进行操作多张表,假如我们遇到以下几种情况:

  • 某个逻辑报错
  • 数据库连接中断
  • 某台服务器突然宕机
  • .......

这时候我们数据库执行的操作可能才到一半,所以为了避免这种一半一半的情况,我们就需要事务来保证数据一致性。 所以事务就是当作一个原子的逻辑组操作,要么全都成功执行,要么全部都失败。事务有分分布式事务和数据库事务,如果没有特指,我们平时所说的事务都是数据库事务,也就是本文探讨的话题。

2. 事务的四大特性

(1) 原子性(Atomicity)

原子可以看作事务的最小单位,而原子性(Atomicity)的概念即要求一组复合操作要构成一个原子,不可在进行分割了,要么都执行成功,要么都不执行直接回滚。

(2) 隔离性(Isolation)

隔离性(Isolation)要求在并发场景下,每个事务之间的操作互不干扰,即我们事务的操作,不会影响到其它是事务的操作结果。

(3) 持久性(Durability)

持久性(Durability):存储到数据库中的数据永不丢失,及时数据库发生故障,当然机器被破坏了那就另说了。

(4) 一致性(Consistency)

一致性是一个比较特殊的概念,和AID不同的是,它并非数据库的特性,按照权威的说法:

ensuring the consistency is the responsibility of user, not DBMS.", "DBMS assumes that consistency holds for each transaction

即一致性要求,从一个正确的状态转换为另一个正确的状态,它并不是DBMS负责的范畴,而是通过DB的AID特定来做到这个C。

我们以转账业务为例说明一下转账操作在系统中的过程:

  • 转账方余额扣除转账的金额。
  • 收款方加上转账的金额。

假设我们手里又90元,希望通过系统转账100到另一个账户上,如果这个操作成功,那么我们的账户就会变为-10元,而另一个账户多了100元。

很明显这种操作并不符合上述所说的从一个正确的状态转为另一个正确的状态,我们必须做到在业务发现转账方余额小于转账额度时,将所有事务中的操作回滚,避免出现上述那种账户余额负数的非正确状态的情况。

这也就是我们上文所说的,通过MySQL的AID来保证C,C是目的,AID是手段,由此保证应用层面业务能够从正确的状态转为另一个正确的状态,以保证业务的约束,从而做到一致性。

3. 并发事务带来那些问题

这里笔者先说一个概念,具体会在后文示例中详尽介绍.

脏读:我们举个例子:

  • 我们开启一个事务A,准备读取user表的数据。
  • 此时,事务B将事务A要读取的数据修改了,但事务还没提交.
  • A却能看到这个未提交的结果即sex为1(而且这个结果后续还不一定提交)。

这种其他事务还没提交的结果能被另一个事务看到的情况就属于脏读。

幻读:我们再举个例子:

  • 事务A查询user表,此时表中有10条数据。
  • 在此期间,事务B插入5条数据。
  • 事务A再次查发现有15条事务。

这种同一次事务两次查询结果不一致的情况是幻读:

不可重复读,仍然举一个例子:

  • 事务A读取id为1的数据,name为xiaoming。
  • 事务B在此期间更新id为1的数据并提交这个事务
  • 结果事务A再次读取时发现name变了。 这就是不可重复读。

你可能会问了,这和幻读听起来是一个概念啊,他俩有什么区别? 幻读说是针对插入或者删除操作后导致数据前后不一致的情况,而不可重复读是针对两次相同查询操作出现数据不一致。也就是说幻读更多是强调前后数据集的不一致和不可重复读更多是强调数据行上的前后不一致。

数据丢失:这个就很好理解了,高并发场景下,事务A修改id为1的money+100,事务B修改id为1的money+200,他们统一时间读取,先后写入,这就导致如果事务A后写入,那么money最后只加了100,如果事务B后写入,那么money就少了100。

二、详解事务的隔离级别

1. 读未提交(READ UNCOMMITTED)

在这个级别下,任何事务的修改操作即使没有提交,其他事务也能看到,造成我们上述所说的脏读,对此我们不妨用下面这段SQL来验证一下:

首先我们先建个测试表:

create table test2 (id int,name varchar(10),money int); 
insert into test2 values(1,'xiaoming',100);
insert into test2 values(2,'xiaowang',100);

事务A开启事务,进行test2  的更新操作,不提交:

start transaction;
-- 小明+100元
update test2   set money = money +100 where name ='xiaoming';
-- 小王减100元
update test2   set money =money -100 where name ='xiaowang';

事务B设置为读未提交的隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
select * from test2 t ;

查询结果是事务B看到了事务A的更新操作,造成脏读。

对应结果如下:

id|name    |money|
--+--------+-----+
 1|xiaoming|  200|
 2|xiaowang|    0|

同理这个读未提交,也会造成:

  • 幻读(同一个事务同一次查询记录数不一样)
  • 不可重复读(同一个事务下查询记录的值不一样)

2. 读已提交(READ COMMITTED)

这个概念也很好理解,每个事务只能看到其他事务提交后的数据。避免了脏读,但是无法避免幻读和不可重复读。 我们就以幻读为例,如下图,事务B首先查询到数据表中没有id为1的用户,在这个查询结束后,事务A进行一次插入操作但是事务还未提交。

然后事务A将数据提交,事务B再次查询就发现了数据,出现幻读:

了解流程之后,我们拿SQL印证一下,首先创建数据表:

drop table if exists account1;


CREATE TABLE `account1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `account1_un` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=UTF8MB4;

事务B查询,没数据:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
START TRANSACTION;

-- 查询表,此时没有数据
SELECT * from account1;

事务A在此期间插入,事务不提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
START TRANSACTION;
-- 在上一个事务查询后,插入一条事务但是不提交
insert into account1(id,name,balance) values(1,'zhangsan',1000);

此时事务B还是没看到数据,然后我们将上述的事务A数据commit,事务B看到这条数据出现幻读:

3. 可重复读(REPEATABLE READ)

这个隔离级别,也很好理解,同一个事务内,多次查询的数据都是一样的。我们不妨基于上面的例子实验一下。

首先事务B查询,没有任何数据:

SET SESSION TRANSACTION ISOLATION LEVEL  REPEATABLE READ;
START TRANSACTION;
select * from account1 a  where id=3;

此时xiaoming的数据为300:

id|name    |balance|
--+--------+-------+
 3|xiaoming|    100|

事务A执行更新并提交:

SET SESSION TRANSACTION ISOLATION LEVEL  REPEATABLE READ;
START TRANSACTION;
update account1 set balance=0 where id=3;
commit;

事务B再查数据还是不变,还是300:

id|name    |balance|
--+--------+-------+
 3|xiaoming|    100|

总的来说可重复读避免了脏读和不可重复读,但是幻读还是无法避免:

4. 串行化(SERIALIZABLE)

事务隔离最高级别,通过锁的方式控制并发流程,解决上述一切问题。

三、详解MVCC(多版本并发控制)

1. 当前读和快照读的基本概念

快照读:即读取数据是从快照中获取的,事务在进行事务读取时不上锁,这就是mysql并发读写性能高的原因之一。

而当前读反之,读取数据时会上锁,这也就意味着即使你的隔离级别是可重复读,你用当前读也能读取到其他事务的最新结果,造成不可重复读。

我们举个例子,首先事务A读取数据,假设数据值是100:

begin;
-- 读取到a的money为100
select * from account1 a ;

事务B更新事务并提交:

update account1 set mnotallow=1000 where id=1;

事务A使用快照读,数据还是100:

select * from account1 a ; --快照读 旧数据

一旦使用当前读,就是其他事务提交的新数据了:

--两个都是当前读,得到最新结果
select * from account1 a for update; 
select * from account1 a lock in share mode;

2. undoLog在事务中的运用

首先说说undo log,在innoDB的聚簇索引中,每一条记录除了我们表中的数据以外,还会额外记录名为事务id(transaction id)的隐藏列。每当用户对当前数据进行修改操作后,新值的数据的事务id就会递增。 同时每行数据还有一个回滚指针(roll_pointer),如下图所示,每当用户对索引进行更新之后,旧的数据就会被存放到undo log中,新的数据的回滚指针指向这条最新的旧数据(就是刚刚存到undo log中的数据,通俗的说是最新的垃圾),用于后续可能需要的回滚操作:

3. readView如何基于undoLog工作

接下来就说说readView,readView就是真正用到undo log的东西,如下图所示,它由三个部分组成,分别是:

  • 已提交事务:已提交事务中记录的则是已经被提交的事务id集合。
  • 活跃事务:这个则记录那些还能活动且还没被提交的事务,其中min_trx_id指向活跃事务的最小值。
  • 未开始事务:这里面则是存放待使用的事务id值,其中max_trx_id就是记录这一块的最小值。

4. 基于可重复读版本理解SQL的MVCC工作机制

了解了undo.log和readView,我们就可以了解mvcc的工作机制了。就先以可重复读RR为例,我们来了解一下如何结合undo.log和readView实现可重复读的。

可重复读这个级别的readView只会在事务刚刚开始时创建,这也就意味着后续数据无论怎么变化,readView都以第一次创建的为主:

假设我们现在account表数据存在一条id为1的数据xiaoming,然后事务trx_id为100的事务基于RR级别将name先更新为xiaoming_50然后再更新为xiaoming_100,但是事务还没提交,此时对应的版本链如下所示:

需要注意的是,只有进行SQL修改操作即insert、update、delete才会分配一个事务id,所以我们本在进行查询之前执行一些无关紧要的update操作,生成一个事务200开始查询执行下面这条sql查询,即查询id为1的数据:

-- 执行一些无关紧要的update
select * from account1 a where id=1;

然后事务启动创建readView,结合版本链记录来看,活跃但是未提交事务值为100,即min_trx_id为100,而我们的事务为200,这也就意味着max_trx_id为201,由此可得活跃未提交的读写事务m_ids列表有100、200之间。

所以事务200生成readView如下,然后顺着版本链开始获取数据首先看到xiaoming_100事务id为100处于活跃事务列表不符合要求继续顺着指针往下走,看到xiaoming_50也不符合要求,继续顺着指针往下走,看到xiaoming事务id值为80小于min_trx_id即已提交的事务中的值,所以我们事务id为200查询结果就是xiaoming:

此时事务100将更新结果提交,因为可重复读生成readView永远是以第一次创建时候为主,这也就意味着查询的思路还是和上述步骤一样,查询结果仍然是trx_id为80的xiaoming,这里就不多做赘述了。

5. 基于读已提交版本readView理解SQL的MVCC工作机制

读已提交版本会在每次执行查询时生成一个readView,我们还是以上面的例子进行演示,还是事务100触发修改但是还没提交,对应生成的版本链如下:

还是同理,执行一些无关紧要的修改操作生成本次的事务id为200然后开始查询,因为事务100没有提交,所以活跃的事务列表数据为100、200生成readView如下:

所以顺着版本链查询到结果也是小于min_trx_id最大值为80,最终查询结果为xiaoming。

然后事务100将结果提交,此时我们的事务200再次进行查询,由读已提交生成readView为每次查询时可得,事务100已提交所以该事务处于已提交事务范围,然后我们的事务200还未提交,所以处于活跃事务列表中,所以活跃事务列表只有我们的事务200:

由此顺着版本链定位到小于min_trx_id的最大值为100,顺着版本链定位到的第一个trx_id为100的结果是xiaoming_100,所以事务200查询结果就是xiaoming_100。

四、关于MySQL事务一些常见问题

1. MySQL 的隔离级别是基于锁实现的吗

是基于锁和mvcc共同实现的,SERIALIZABLE 这个隔离级别就是基于锁实现的,其他隔离级别都是基于mvcc,需要补充的是REPEATABLE-READ 如果使用当前读也是基于锁实现。

2. MySQL 的默认隔离级别是什么

以笔者使用的MySQL8来说使用如下命令可以看到默认级别为可重复读:

select @@transaction_isolation;

对应输出结果如下:

@@transaction_isolation|
-----------------------+
REPEATABLE-READ        |

五、小结

MySQL 的 MVCC(多版本并发控制)是其实现高效并发处理的关键机制。

通过 MVCC,在并发读写操作时,读操作不会阻塞写操作,写操作也不会阻塞读操作,极大地提高了数据库的并发性和性能。

它允许事务读取到特定版本的数据,实现了事务隔离级别的灵活控制。使得不同的事务可以看到符合其隔离级别要求的数据视图。

在 MVCC 中,每行数据都有多个版本,记录了不同事务对其的修改历史。这种方式有效地避免了锁竞争带来的性能开销和潜在的死锁问题。

对于理解和优化数据库的并发操作,MVCC 是一个至关重要的概念。深入研究和掌握它,有助于更好地设计和管理数据库系统,确保数据的一致性和高效性。

本文地址:https://www.yitenyun.com/216.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK 双主架构 循环复制 Web 应用 异步数据库 序列 核心机制 生命周期 Deepseek 宝塔面板 Linux宝塔 Docker JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS Windows宝塔 Mysql重置密码 无法访问宝塔面板 HTTPS加密 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 ES 协同 修改DNS Centos7如何修改DNS scp Linux的scp怎么用 scp上传 scp下载 scp命令 防火墙 服务器 黑客 Serverless 无服务器 语言 存储 Spring SQL 动态查询 Oracle 处理机制 Linux 安全 网络架构 工具 网络配置 加密 场景 MySQL 9.3 开源 PostgreSQL 存储引擎 RocketMQ 长轮询 配置 HexHub Canal Rsync 架构 InnoDB 缓存方案 缓存架构 缓存穿透 信息化 智能运维 响应模型 日志文件 MIXED 3 线上 库存 预扣 索引 数据 业务 监控 聚簇 非聚簇 B+Tree ID 字段 AI 助手 数据库锁 单点故障 分库 分表 云原生 DBMS 管理系统 Redis 自定义序列化 SpringAI 优化 万能公式 Redis 8.0 GreatSQL Hash 字段 SVM Embedding PostGIS 系统 SQLark openHalo OB 单机版 数据集成工具 SQLite Redka ​Redis 机器学习 推荐模型 sqlmock 虚拟服务器 虚拟机 内存 Netstat Linux 服务器 端口 分页查询 自动重启 运维 同城 双活 缓存 sftp 服务器 参数 共享锁 • 索引 • 数据库 RDB AOF 排行榜 排序 Testcloud 云端自动化 查询 prometheus Alert SQLite-Web 数据库管理工具 不宕机 容器化 Postgres OTel Iceberg 分布式架构 分布式锁​ 聚簇索引 非聚簇索引 OAuth2 Token Entity 开发 StarRocks 数据仓库 技术 EasyExcel MySQL8 向量数据库 大模型 IT AIOPS 数据备份 MongoDB 容器 数据类型 Doris SeaTunnel 人工智能 推荐系统 分页 数据结构 连接控制 机制 Caffeine CP Python Web LRU Milvus IT运维 部署 崖山 新版本 高可用 向量库 悲观锁 乐观锁 Ftp 池化技术 连接池 磁盘架构 MCP mini-redis INCR指令 redo log 重做日志 MVCC 事务隔离 流量 开放协议 Web 接口 字典 电商 单线程 线程 数据脱敏 加密算法 R2DBC QPS 高并发 原子性 对象 微软 SQL Server AI功能 速度 服务器中毒 窗口 函数 双引擎 RAG HelixDB 频繁 Codis Crash 代码 主库 Order SSH 网络 dbt 数据转换工具 1 PG DBA ZODB 引擎 性能 List 类型 Pottery 工具链 优化器 InfluxDB 模型 意向锁 记录锁 网络故障 传统数据库 向量化 发件箱模式 事务同步 UUIDv7 主键 仪表盘 Redisson 锁芯 线程安全 INSERT COMPACT Undo Log LLM 连接数 订单 JOIN