当前位置: 首页 > news >正文

深入解析Mysql数据库并发:从读写机制到多版本控制

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、数据库并发的三种场景
  • 二、读-写
    • 2.1 三个隐藏字段
    • 2.2 多版本控制(mvcc)
    • 2.3 Read View
      • 2.3.1 Read View 的核心作用
      • 2.3.2 Read View 的本质:可见性判断的“规则集合”
      • 2.3.3 简化后的 Read View 关键结构(理解核心逻辑)
    • 2.4 整体流程
    • 2.5 RC和RR的区别


前言

本篇文章作为拓展内容,是对MySQL事务隔离性的更深入理解,建议在过上篇文章的基础上阅读。


Mysql数据库事务全解析:概念、操作与隔离级别

一、数据库并发的三种场景

通过上篇的学习,可以知道数据库并发场景可分为以下三类,其冲突风险和处理需求各不相同:

  1. 读-读并发
    多个事务同时读取同一份数据,不存在任何冲突或安全问题,无需额外的并发控制机制。因为读取操作不会修改数据,彼此间不会相互干扰。

  2. 读-写并发
    一个事务读取数据,同时另一个事务修改该数据,存在线程安全风险,可能破坏事务隔离性,引发下列问题:

    • 脏读(读取到未提交的修改)
    • 不可重复读(同一事务内多次读取结果不一致)
    • 幻读(同一条件下多次读取的记录数量不同) 需通过隔离级别和锁机制控制可见性,避免此类问题。
  3. 写-写并发
    多个事务同时修改相同数据,存在线程安全风险,可能导致更新丢失(后一个事务的修改覆盖前一个事务的修改,导致部分更新结果丢失)。需通过锁机制(如行锁、间隙锁)保证修改的原子性和顺序性,防止冲突。

读-读:并不会产生并发问题,所以接下来不做介绍

二、读-写

在这里插入图片描述
通过上篇的学习,我们可以知道对于读-写产生的一系列问题,并不是通过加锁解决的,那么它是如何办到的呢?
在MySQL中采用了,多版本并发控制( MVCC )的技术来解决读-写冲突的无锁并发控制

在正式理解mvcc前我们首先需要理解三个前提知识:

  • 3个记录隐藏字段
  • undo 日志
  • Read View

2.1 三个隐藏字段

在 MySQL 的 InnoDB 存储引擎中,创建表时会自动为每条记录添加以下隐藏字段(无需手动定义):
示例(简化)

nameageDB_TRX_ID(创建事务ID)DB_ROW_ID(隐藏主键)DB_ROLL_PTR(回滚指针)
张三28111指向 undo log 空值(无历史版本)
  1. DB_TRX_ID(6字节)
    作用:记录最近修改(包括插入、更新)该记录的事务ID。每个事务启动时,InnoDB 会分配一个唯一的事务ID,通过此字段可追踪记录的修改来源。

理解事务ID可以结合文件描述符理解,当有事务产生时,MySQL 会为这个事务分配一个事务ID,ID值是递增的,越小代表事务产生的越早。

  1. DB_ROLL_PTR(7字节)
    作用:回滚指针,指向该记录的上一个历史版本。这些历史版本存储在 undo log(回滚日志)中,用于事务回滚或 MVCC(多版本并发控制)的一致性读。

    我们介绍过,MySQL服务启动时会开辟一块内存,当作缓冲区,而在这块缓冲区中有一段叫做undo log的空间:
    在这里插入图片描述当不断的有事务对数据修改时,就可以形成一条版本链,而回滚指针可以帮助我们快速定位旧版本,完成回滚操作。
    补充:当该记录被事务ID=11的事务更新后,DB_TRX_ID 会变为11,DB_ROLL_PTR 指向更新前的版本

  2. DB_ROW_ID(6字节)
    作用:隐含的自增ID(隐藏主键)。若表未定义主键或非空唯一键,InnoDB 会以该字段作为聚簇索引的键,保证每条记录有唯一标识。
    这一字段我们很早自建就介绍了

补充说明

  • 此外,还存在一个删除标记(delete flag) 隐藏字段:记录被删除或更新时,并非物理删除,而是通过此字段标记为“已删除”(如:0表示删除、1表示有效)。历史版本仍保留在 undo log 中,直至被清理。

这里不想细讲,但是有一件事情得说清楚, MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的.所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

2.2 多版本控制(mvcc)

MVCC(多版本并发控制)的核心机制是:为事务分配单向增长的事务ID,为数据的每一次修改保存一个版本,且版本与事务ID关联;读操作仅读取事务开始前的数据库快照

基于这一机制,MVCC为数据库解决了以下关键问题:

  1. 提升并发性能
    并发读写时,读操作无需阻塞写操作,写操作也无需阻塞读操作,通过多版本隔离实现无锁并发,提高数据库的读写效率。

  2. 解决事务隔离问题
    可有效避免脏读、不可重复读、幻读等隔离性问题——读操作基于快照,不受其他未提交事务的修改影响,保证事务内数据的一致性。

我们通过一个实例模拟 MVCC 的工作过程:

假设有一个事务10(ID为10,仅为区分),需对 student 表中的一条记录执行修改操作(将 name='张三' 改为 name='李四'),过程如下:

  1. 加锁:事务10因要修改数据,首先对该记录施加行锁,防止其他事务同时修改。
  2. 版本备份(写时拷贝):修改前,先将该记录的当前版本完整拷贝到 undo log(回滚日志)中,形成一条副本数据(作为历史版本)。
  3. 修改原始记录
    • 直接更新原始记录的 name 字段为 '李四'
    • 同步更新原始记录的隐藏字段:
      • DB_TRX_ID 设为当前事务ID(10),标记该记录最后由事务10修改;
      • DB_ROLL_PTR 写入 undo log 中副本数据的地址,使其指向该历史版本(表示“当前版本的上一版本是副本”)。
  4. 提交与释放锁:事务10执行 commit 提交后,释放行锁,修改正式生效。

在这里插入图片描述
接着前面的场景,现在有一个事务11(ID为11),对 student 表中同一条记录(即事务10修改过的那条 name='李四' 的记录)执行修改操作:将 age=28 改为 age=38,过程如下:

  1. 加锁:事务11因要修改该记录,需先获取该记录的行锁(若事务10未释放锁,事务11会等待锁释放)。
  2. 版本备份(写时拷贝):修改前,将该记录的当前版本(即事务10修改后的版本)再次拷贝到 undo log 中,新增一条副本数据。此时,undo log 中的版本链采用头插方式更新——新副本作为最新的历史版本,排在链的前端。
  3. 修改原始记录
    • 直接更新原始记录的 age 字段为 38
    • 同步更新隐藏字段:
      • DB_TRX_ID 设为当前事务ID(11),标记该记录最后由事务11修改;
      • DB_ROLL_PTR 指向 undo log 中刚新增的副本数据地址,即“当前版本的上一版本是事务10修改后的版本”。
  4. 提交与释放锁:事务11执行 commit 提交后,释放行锁,修改生效。

此时,undo log 中已形成一条版本链:事务11修改后的当前版本 → 指向事务10修改后的版本(副本1)→ 指向事务10修改前的原始版本(副本0),通过 DB_ROLL_PTR 依次串联:
在这里插入图片描述

如果是insert呢?因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。
总结一下,也就是我们可以理解成,updatedelete可以形成版本链,insert暂时不考虑
select读取,是读取最新的版本呢?还是读取历史版本?
当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in sharemode(共享锁), select for update (这个好理解,我们后面不讨论)
快照读:读取历史版本(一般而言),就叫做快照读。(这个我们后面重点讨论)
这段内容是在解释 MVCC(多版本并发控制)解决并发读写冲突、提升效率的核心逻辑,可以拆成两部分理解:

“当前读 + 加锁”的问题(无 MVCC 时的困境)
当多个事务同时修改数据(当前读,即读取并修改最新版本)时,数据库需要用行锁/表锁保证数据一致性:
比如事务 A 要修改某条记录,会先给记录加锁;此时其他事务(不管是修改还是想读“最新版”)都得等锁释放,相当于串行执行,效率很低。

MVCC 的“快照读”优势(解决串行化问题)
MVCC 引入 “快照读” 机制:
当查询需要读数据时,不强制读取“最新加锁版本”,而是可以读 历史版本(这些版本存在 undo log 里,是修改前的副本); 读历史版本时不需要加锁,多个查询可以同时读不同版本的历史数据,实现 并行执行,避免了“读等待写锁”的串行化问题。

虽然写操作(当前读)仍需要加锁保证一致性,但读操作(快照读)通过访问 undo log 的历史版本,绕开了写锁的阻塞
这样就实现了 “写-写互斥(加锁保证一致性),读-写并行(快照读无锁)”,提升并发效率——这就是 MVCC 的核心价值。

简单说:MVCC 让“读”不用等“写”的锁,用历史版本实现并行读,解决了纯加锁串行化的低效问题。

“当前读/快照读”的开关: 隔离级别 ,事务的隔离级别,决定了 SELECT 是“当前读”还是“快照读”
比如:

  • 读未提交(Read Uncommitted)、可重复读(Repeatable Read,MySQL InnoDB 默认)等不同隔离级别下,SELECT 的行为不同——有的强制读最新版本(当前读,需加锁),有的则允许读历史版本(快照读,无锁)。

“隔离级别”存在的意义:解决事务交织执行时,数据的可见性问题
事务本身是原子性的(BEGINCRUDCOMMIT 是一个完整周期 ),但实际运行时,多个事务的“执行阶段(执行前、执行中、执行后)会相互交织

  • 事务 A 执行 UPDATE 时,事务 B 可能正在执行 SELECT
  • 事务 C 刚 BEGIN,事务 D 可能已经 COMMIT

为了让 “有先后顺序的事务,能看到「符合逻辑的正确数据」” ,就需要一套规则定义:不同事务在执行中相互交织时,彼此该“看到什么、看不到什么” ——这就是 “事务隔离性” 的本质,而 “隔离级别”就是这套规则的具体细化(比如读未提交、读已提交、可重复读、串行化)。

隔离级别是为了在“多事务并发交织执行”的场景下,精准控制事务间的可见性:让每个事务既不被其他事务的干扰破坏逻辑,又能尽可能并行执行(提升效率)。而“当前读/快照读”的选择,正是隔离级别落地的具体体现(不同隔离级别下,SELECT 策略不同)。

如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?

2.3 Read View

2.3.1 Read View 的核心作用

Read View(读视图)是事务执行「快照读」时生成的关键结构

  • 当事务触发快照读(比如可重复读隔离级别的 SELECT),MySQL 会在“读的瞬间”生成一个 数据库当前状态的“快照视图”
  • 这个视图会记录并维护“系统当前活跃事务的 ID”(事务 ID 是全局递增的,新事务 ID 更大 )。

活跃事务:当前并发未提交的事务

2.3.2 Read View 的本质:可见性判断的“规则集合”

在 MySQL 源码中,Read View 对应一个 类(数据结构) ,核心职责是 “判断某条记录的版本对当前事务是否可见”

  • 执行快照读时,对目标记录生成 Read View,相当于一套“可见性规则”;
  • 通过对比记录的事务 ID(或其 undo log 历史版本的事务 ID )与 Read View 的规则,决定返回“当前记录”还是“undo log 里的历史版本”。

2.3.3 简化后的 Read View 关键结构(理解核心逻辑)

实际源码更复杂,我们聚焦关键字段的作用:

class ReadView {
private:// 高水位:事务 ID ≥ 此值 → 对当前事务“不可见”//ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没
有写错)trx_id_t m_low_limit_id;  // 低水位:事务 ID < 此值 → 对当前事务“可见”//记录m_ids列表中事务ID最小的ID(没有写错)trx_id_t m_up_limit_id;  // 创建该 Read View 的“当前事务 ID”trx_id_t m_creator_trx_id;  // 生成视图时,系统中“活跃事务的 ID 列表”ids_t m_ids;  // (辅助清理)标记无需保留的 undo log 版本,小于此值的可被 purgetrx_id_t m_low_limit_no;  // 标记视图是否已关闭(生命周期管理)bool m_closed;  
};

核心逻辑

  • m_low_limit_id(高水位)和 m_up_limit_id(低水位)划定“可见事务 ID 的范围”;
  • 结合 m_creator_trx_id(当前事务 ID)和 m_ids(活跃事务 ID 列表),判断一条记录的版本是否对当前事务可见——如果可见,直接读当前记录;如果不可见,去 undo log 找历史版本再判断。

我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID 。那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的 DB_TRX_ID 。所以现在的问题就是,当前快照读,应不应该读到当前版本记录。

在这里插入图片描述
下面为是否可见的比较规则
在这里插入图片描述

如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的 readview 是当你进行select的时候,会自动形成。

2.4 整体流程

以可重复读为背景
在这里插入图片描述
在这里插入图片描述

  • 事务4:修改name(张三) 变成name(李四)
  • 当 事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2

在这里插入图片描述

只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以此时m_ids表中没有事务4的ID,即,事务4相对于事务2可见。
在这里插入图片描述

我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。

//比较步骤
DB_TRX_ID(4< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。
//结论
故,事务4的更改,应该看到。所以事务2能读到的最新数据记录是事务4所提交
的版本,而事务4提交的版本也是全局角度上最新的版本

2.5 RC和RR的区别

我们之说过,执行快照读时版本是否当前可见,是由隔离级别决定的,那么它是如何实现的呢?
不同隔离级别对Read View的创建规则是不同的。

  • 核心区别:Read View生成时机不同
    这是RC(读已提交)和RR(可重复读)隔离级别下快照读结果差异的本质原因。

  • RR级别下的Read View机制

    • 同一事务中,对某条记录的第一次快照读会创建快照及Read View,记录当前系统中所有活跃的其他事务。
    • 此后该事务内的所有快照读,均复用同一个Read View
    • 因此,若当前事务在其他事务提交更新前已执行过快照读,后续其他事务的修改对其不可见(保证重复读一致性)。
    • 可见性规则:早于Read View创建的事务所做的修改可见;Read View记录的活跃事务的修改不可见
  • RC级别下的Read View机制

    • 同一事务中,每次执行快照读都会新生成快照及Read View(获取最新的活跃事务状态)。
    • 因此,其他事务提交的更新会被当前事务的快照读感知到(导致不可重复读)。
  • 隔离性表现差异的根源

    • RC因“每次快照读生成新Read View”,导致同一事务中多次快照读可能看到不同结果(不可重复读)。
    • RR因“同一事务内复用首个Read View”,保证同一事务中多次快照读结果一致(可重复读)。

大家可以测试一下

http://www.dtcms.com/a/390501.html

相关文章:

  • Python自学20 - Python操作PDF文件
  • Windows 本地 UV 环境部署 Index-TTS2 实战:基于 EPGF 架构的完整指南(支持 DeepSpeed + FP16)
  • 【cpp Trip第4站】函数参数传递
  • 【Spotfire】实现错行效果
  • 【Day 61】Linux-haproxy负载均衡
  • 搭建线上线下融合的商城小程序,关键步骤有哪些?
  • 软件测试教程资源合集
  • Proteus(8.17)SP2 仿真下载安装过程(附详细安装过程图)
  • 微软获评 2025 Gartner 云原生应用平台魔力象限 领导者
  • Java 生态监控体系实战:Prometheus+Grafana+SkyWalking 整合全指南(一)
  • 【学习】响应系统
  • Linux网络:socket网络套接字
  • 知识图谱对人工智能中自然语言处理的深层语义分析的影响与启示
  • 从车间到云端:Kepware如何加速IIoT落地
  • MyISAM 与 InnoDB 深度对比:如何正确选择 MySQL 存储引擎
  • 串口无线数传模块实现化工园区与3公里外水泵PLC无线通讯实现设备数据传输
  • rook-ceph自定义添加osd流程
  • 【需求导向】解读660页智慧教育大数据信息化顶层设计及智慧应用建设方案
  • InnoDB 引擎深潜指南---从逻辑结构到 MVCC 原理,带源码级案例与性能要点
  • Android Compose 开发 界面间的跳转(Router)
  • unity(C#/cs)请求 python django后端服务器预制体渲染 scroll list 视频列表
  • 《Linux 指令实战进阶:从终端新手到 shell 驾驭者的技术跃迁(第三篇)》
  • 临床AI产品化全流程研究:环境聆听、在环校验与可追溯系统的多技术融合实践(下)
  • Croe 11.0 学习笔记:1.5 草绘
  • Hadoop 1.x 与 2.x 版本对比:架构演进与核心差异解析
  • 【5/20】Express.js 基础:构建 RESTful API,实现用户数据端点
  • SmartX 榫卯企业云平台+ StarRocks 大数据联合解决方案
  • CodeX 新王已来:从技术原理到工程实践,AI 如何重构编程全流程
  • 智慧赋能:King‘s Biobank 重构生物样本管理新范式
  • Chromium 138 编译指南 Ubuntu 篇:环境配置与基础准备(一)