云计算百科
云计算领域专业知识百科平台

MySQL 底层执行原理与实现历程

本文完整梳理一条 UPDATE 在 MySQL 中的执行链路(连接池 → Server 层 → InnoDB 内存 → Undo / Redo / Binlog → 异步落盘),以及 MySQL 与 InnoDB 的演进历程。理解这条链路,是回答两个问题的基础:事务提交后数据为什么不会丢?以及崩溃恢复依赖什么机制?

一、连接池让高并发下不能每次都建连

任何 SQL 的第一步都是建立连接。连接是稀缺且昂贵的资源,高并发下让业务线程各自开关连接会造成巨大开销。本节说明连接池为何是必选项。

Java 应用通过 JDBC 驱动把 SQL 发往 MySQL。一个运行在 Tomcat 中的 Web 系统,多个线程并发处理请求;若每个线程都走"建连接 → 执行 → 断连接",高并发下连接的频繁创建与销毁会让握手与鉴权开销被显著放大。

MySQL 与应用两侧都依赖连接池:预先建立一组连接,线程按需取用、用完归还。它带来三点收益:连接复用省去反复握手;连接数可控,避免拖垮数据库资源;管理简化,无需在业务代码里手动开关连接。常见的服务端连接池配置集中在 max_connections、wait_timeout、interactive_timeout,应用侧则依赖 HikariCP、Druid 等实现。

在这里插入图片描述
连接就绪后,请求进入 Server 层,看它如何消化这条 SQL。

二、Server 层中SQL 如何被解析与调度

连接建立后,服务线程接管请求。本层不碰数据文件,只把 SQL 翻译成执行方案交给引擎。

一条网络请求到达 MySQL 后,处理链路固定:

  • 分配线程,监听并读取连接中的 SQL 语句
  • SQL Interface 接收该语句
  • 解析器解析SQL
  • 查询优化器生成查询路径树,选出代价最小的执行方案
  • 执行器按执行计划调度
  • 存储引擎(默认 InnoDB)真正读写数据
  • 概括为:接口接收 →解析器解析→ 优化器选路 → 执行器调度 → 引擎落地。
    请添加图片描述

    一点历史细节:MySQL 5.7 及之前存在查询缓存(Query Cache),以表为单位缓存 SELECT 结果,但任意对该表的写(包括 UPDATE)都会让整张表的缓存失效,写多场景下反而拖累性能,因此默认关闭;MySQL 8.0 已彻底移除该功能。

    计划定下后,读写交给存储引擎,InnoDB 的内存结构自此介入。

    三、Buffer Pool是InnoDB 的内存心脏

    InnoDB 读写多先发生在内存,Buffer Pool 是其性能核心。正因内存易失,才引出 Undo、Redo 与刷盘机制。本节先讲基础,再补 Change Buffer、Double Write 与三条链表。

    InnoDB 将大量数据页缓存在内存的 Buffer Pool 中,以减少直接读磁盘的次数。

    以如下语句为例:

    — 场景:将 id=1 这行用户的 name 改为 xxx
    UPDATE users SET name = 'xxx' WHERE id = 1;

    InnoDB 依次做三件事:检查 id=1 所在数据页是否在 Buffer Pool,不在则从磁盘加载;对该行加独占锁,防止并发修改;在内存中把 name 改为 xxx。

    此时内存中的数据已与磁盘不一致,称为"脏页"。Buffer Pool 用 LRU 算法管理数据页,保留热点数据在内存。专用数据库服务器建议将 innodb_buffer_pool_size 设为物理内存的 60%~75%,缓存命中率直接决定查询性能。

    Change Buffer:非唯一索引的更新优化。 对于非唯一二级索引的改动,如果目标数据页不在 Buffer Pool,InnoDB 不会立即把页读入内存,而是先把变更记入 Change Buffer,等后续该页被读入时再合并(merge)。唯一索引因插入时必须立即判重,无法使用 Change Buffer。其行为由 innodb_change_buffering 控制(默认 all,即增删改都缓冲)。

    脏页刷盘时机与 Double Write Buffer。 脏页由后台 IO 线程异步刷回磁盘,触发条件包括:Buffer Pool 空间不足、Redo Log 写满需要推进 checkpoint、后台定期刷写。写脏页时,InnoDB 先顺序写入 Double Write Buffer(内存 2MB 加 ibdata 中连续 128 页),再写真正的数据文件,用以防范"部分写失效"(16KB 页写到一半宕机导致页损坏);崩溃恢复时用副本修复。默认 innodb_doublewrite = ON。

    Buffer Pool 的三条链表。 Buffer Pool 内部用三条链表管理页:

    • free list:管理尚未使用的空闲页,分配新页时从这里取;
    • LRU list:缓存页按改进 LRU 组织——新读入的页插入 LRU 中点(old 区)而非队首,避免全表扫描把热点页冲掉;old 区比例由 innodb_old_blocks_pct(默认 37%)控制,页在 old 区停留超过 innodb_old_blocks_time 才晋升 young 区;
    • flush list:脏页按 oldest_modification LSN 升序排列,后台线程依此顺序刷盘。

    内存改完但仍易失,宕机即丢,于是需要 Undo 留退路、Redo 保安全,先看 Undo Log。

    四、回滚的Undo Log日志

    改内存前须留退路。事务原子性与并发快照读都靠 Undo Log 支撑。本节说明其作用。

    在修改内存之前,InnoDB 先把修改前的旧值写入 Undo Log。

    它的作用直接:事务回滚时,依靠 Undo Log 将数据逆向还原到事务开始前的状态,这是事务原子性的底层支撑。此外,MVCC 多版本快照也依赖 Undo Log 存储历史版本,使读请求能拿到一致的快照而不被写阻塞。Undo Log 分两类:insert undo(插入产生的,事务提交后可直接丢弃)与 update undo(更新/删除产生的,需保留供 MVCC 历史版本,由 purge 线程在不再被引用后清理)。

    Undo 解决回滚与快照读,但没解决崩溃不丢。真正兜底的是 Redo Log。

    五、MVCC 与事务隔离级别

    Undo Log 支撑 MVCC。MVCC 靠行版本链与 ReadView 实现读写不阻塞,隔离级别决定可见哪些版本。本节再补锁机制,说明并发修改如何被约束。

    InnoDB 每行记录带有隐藏列:DB_TRX_ID(最后一次修改该行的事务 ID)与 DB_ROLL_PTR(回滚指针,指向 Undo Log 中的历史版本)。事务发起读时,InnoDB 生成 ReadView,依据各版本的事务 ID 判断可见性:只有"已提交且不在当前事务活跃集合"的版本才可见,从而拿到一致性快照。

    四种标准隔离级别的差异如下:

    隔离级别快照粒度典型问题实现要点
    READ UNCOMMITTED 无快照 脏读(读到未提交) 直接读最新
    READ COMMITTED 语句级 不可重复读 每条语句新建 ReadView
    REPEATABLE READ(默认) 事务级 幻读(靠 Gap 锁缓解) 事务首次读建 ReadView,全程不变
    SERIALIZABLE 串行 读加共享锁,等价于串行

    锁:行锁、间隙锁与 Next-Key Lock。 MVCC 解决读写不阻塞,写写冲突靠锁约束:

    • Record Lock(记录锁):锁住索引上的具体记录;
    • Gap Lock(间隙锁):锁住记录之间的间隙,防止其他事务插入,用于解决幻读;
    • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定记录及前面间隙(左开右闭),是 RR 级别下的默认行锁算法;
    • 意向锁 IS / IX:表级锁,协调表锁与行锁,避免逐行检查。

    RC 级别下通常只使用 Record Lock(唯一索引等值查询除外),RR 级别下使用 Next-Key Lock。隔离级别决定可见哪些版本,但这些版本仍在内存中,宕机即丢,恢复靠Redo Log。

    六、Redo Log:崩溃也不丢的底气

    内存改完未落盘,宕机即丢。Redo Log 用顺序写 + 崩溃重放换取提交即可不丢。本节讲原理、WAL/ARIES、文件结构与 LSN/Checkpoint。

    Redo Log 是一份物理日志,记录"对哪个数据页的哪条记录做了什么修改",有两个作用:崩溃后重放 Redo Log,可恢复到最后一次提交的状态;把随机写磁盘转为顺序写日志,提升写入吞吐。

    WAL 与 ARIES 原则。 Redo Log 的理论基础是 WAL(Write-Ahead Logging,先写日志):事务提交时,其对应的 Redo 必须先落盘(force log at commit),数据页可延后刷盘。这样用顺序日志写替代随机页写,崩溃后靠重放恢复。InnoDB 的实现思想源自 IBM 的 ARIES 算法,核心三条:WAL(先写日志)、Repeat History(重放时连未提交事务的操作一起重放)、Undo 用日志记录的逆操作回滚。工程上 InnoDB 采用 steal(脏页可提前刷盘)+ no-force(事务提交不必立即刷数据页),持久性完全由 Redo 保证。

    文件结构与循环写。 Redo Log 由一组文件(如 ib_logfile0、ib_logfile1)组成,由 innodb_log_files_in_group 与 innodb_log_file_size 决定大小,环状循环写入。文件上有 write pos(当前写入位置)和 checkpoint(已刷盘脏页对应的重放点),二者之间的区域是可用空间;checkpoint 推进表示对应脏页已落盘,该段 Redo 可被覆盖。

    LSN 与 Checkpoint。 LSN(Log Sequence Number)是单调递增的日志序号,数据页、Redo 记录、Checkpoint 都带 LSN。Checkpoint 记录"已刷盘脏页对应的最老 LSN",崩溃恢复时只需重放该 LSN 之后的 Redo。运行期采用 fuzzy checkpoint(master thread、flush list、脏页过多等触发),不阻塞写入;恢复阶段比较页 LSN 与 Redo LSN,仅重放页 LSN 之后的记录。

    刷盘策略。 修改发生时先写入内存的 Redo Log Buffer(默认 8MB,由 innodb_log_buffer_size 控制),事务提交时刷盘。刷盘策略由 innodb_flush_log_at_trx_commit 控制:

    值行为取舍
    0 提交时仅写入 OS 缓存,不立即刷盘 性能最好,崩溃可能丢数据
    1(默认) 提交时立即刷盘并等待 IO 完成 最安全,性能略有损耗
    2 提交时写入 OS 缓存,由后台异步刷盘 性能与保护折中,仍有风险

    线上核心业务建议设为 1,提交即落盘,严格保证数据不丢。

    崩溃恢复时如何重放。 重启后 InnoDB 进入恢复阶段:先用 Redo Log 把记录重放到内存(含已提交和崩溃瞬间未完成的操作,即 Repeat History),再对 Redo 中没有 commit 标记的事务,用 Undo Log 回滚。两阶段提交保证 Redo 与 Binlog 一致:提交时先写 Redo(prepare,无 commit 标记)、再写 Binlog、最后在 Redo 写 commit 标记;恢复时若 Binlog 已完整写入则补提交,否则回滚。

    Redo 保引擎层不丢,但主从复制与恢复依赖 Server 层的 Binlog。

    七、Binlog:Server 层的归档日志

    Binlog 是 Server 层逻辑日志,与引擎无关,可跨引擎用于复制与恢复。

    Binlog 记录"对 users 表 id=1 那行做了更新,更新后的值是什么",归属 Server 层。Binlog 刷盘策略由 sync_binlog 控制,默认 0(先进入 OS 缓存,宕机可能丢),设为 1 则提交时强制落盘。

    Binlog 有三种格式:

    格式记录内容优点缺点
    STATEMENT 原始 SQL 语句 日志量小 now()/uuid()/limit 等不确定函数致主从不一致
    ROW(默认) 每行前后像 主从绝对一致,最安全 日志量大
    MIXED 自动切换 兼顾空间与正确 行为依赖优化器判断

    两阶段提交:让 Redo 与 Binlog 保持一致。 提交事务、Binlog 落盘后,InnoDB 还会做关键一步:将本次更新的 Binlog 文件名与位置写入 Redo Log,并在 Redo Log 中写入 commit 标记。只有 Redo Log 中出现了这个标记,事务才算真正提交成功。这一步的意义在于:保证 Redo Log 与 Binlog 两侧都完整记录了这次更新,避免出现"一边有、一边没有"的不一致。在内部实现上,这是一个 XA 事务,由 Server 层统一协调 Redo 与 Binlog 的提交顺序。

    最后,后台 IO 线程会在之后的某个时机,将 Buffer Pool 中的脏页异步刷回磁盘。即便在刷盘前崩溃,重启后也会依据 Redo Log 恢复内存中的修改,待时机成熟再刷盘。

    Redo 与 Binlog 对齐、脏页落盘,链路闭合。
    为啥采用2PC? 不用 2PC,两种错误执行顺序都会出数据错乱
    方案 A:先写 Redo 提交,再写 Binlog

    • 时序:更新内存 → Redo 刷盘标记 COMMIT → 宕机 → Binlog 还没写
    • 本地 InnoDB:重启回放Redo,这条更新存在;
    • Binlog:无这条记录,从库 / 备份不会同步该更新;

    结果:主库多数据,从库少数据,主从不一致。

    方案 B:先写 Binlog,再写 Redo 提交

    • 时序:更新内存 → Binlog 完整落盘 → 宕机 → Redo 没标记提交
    • Binlog 已持久化,从库已经同步执行更新;
    • InnoDB 重启后,Redo 无提交标记,直接回滚事务;

    结果:主库少数据,从库多数据,主从不一致。

    关键矛盾
    两个 IO 操作中间随时宕机,必然出现一份日志有事务、另一份没有。
    业务、主从、定时备份全部错乱,数据完全不可信。
    单纯串行顺序无法解决 “中间宕机” 的原子性问题,必须引入协调机制,也就是两阶段提交。

    为啥 Binlog 在 server 层?

    • Binlog 是全引擎通用、实例全局的复制 / 备份日志,MyISAM 等无事务引擎也需要;
    • Binlog 记录 SQL 逻辑变更,由Server 层 SQL 执行器统一生成; 主从复制、时间点恢复属于 MySQL 顶层服务能力,不属于单个存储引擎;
    • 2PC 协议需要Server 作为协调者持有 Binlog,才能协调 InnoDB 的 Redo 保证一致性;
    • 分层架构职责解耦,通用能力放Server,引擎专属事务 / 物理日志下沉引擎。

    八、一条 UPDATE 的完整时间线

    把前面各组件按时间顺序排成一张表,建立端到端认知,共九个步骤。

    步骤组件动作
    1 JDBC + 连接池 应用发送 SQL,获取连接
    2 网络 + 线程 MySQL 分配线程,读取并解析 SQL
    3 Server 层 接口接收 → 解析器解析 → 优化器选路 → 执行器调度
    4 InnoDB 加载数据页进 Buffer Pool,加独占锁
    5 Undo Log 记录修改前旧值
    6 Buffer Pool 内存中改为新值(产生脏页)
    7 Redo Log Buffer 记录物理修改
    8 提交事务 Redo 刷盘 → Binlog 刷盘 → Redo 写 commit 标记
    9 后台 IO 线程 后续某时机将脏页刷回磁盘

    在这里插入图片描述
    这张表是全文骨架,跑稳机制还需正确配置参数。

    九、落地到配置:关键参数

    原理落到参数。下面汇总影响性能与安全的关键参数及取舍。

    参数推荐值说明
    innodb_buffer_pool_size 物理内存 60%~75% 缓存命中率直接决定性能
    innodb_log_file_size 512M~1G(权衡恢复时长) 越大写吞吐越高,但崩溃恢复越慢
    innodb_flush_log_at_trx_commit 1 事务提交 Redo 即落盘,不丢数据
    sync_binlog 1 事务提交 Binlog 即落盘,主从一致
    innodb_doublewrite ON 双写防部分写失效,一般不关
    innodb_change_buffering all 非唯一索引改动进 Change Buffer
    innodb_io_capacity 按磁盘(SSD 2000~4000) 后台刷脏页的 IO 能力上限

    核心业务务必把 innodb_flush_log_at_trx_commit 与 sync_binlog 都设为 1,以换取严格不丢;吞吐敏感且可容忍少量丢失的场景才考虑放宽。

    十、实现历程:MySQL 与 InnoDB 的演进

    理解底层原理,也需知道这套架构从何而来。MySQL 自设计之初就把 Server 层与存储引擎解耦(插件化引擎),Server 负责 SQL 解析、优化、日志,引擎负责数据存储,InnoDB 正是这一架构下最关键的引擎。

    关键节点:

    • 1995 年:MySQL 由 Michael “Monty” Widenius 等人创立(MySQL AB,瑞典),以 C/C++ 编写,最初作为 mSQL 的替代。
    • 早期默认引擎为 MyISAM:表级锁、非事务、无崩溃恢复,读多写少场景表现好但有明显短板。
    • InnoDB 由 Heikki Tuuri 创立的 Innobase Oy(芬兰)开发,支持事务、行锁、MVCC、外键;2000 年前后作为可选引擎集成进 MySQL,5.0 起趋于成熟。
    • 2005 年 Oracle 收购 Innobase;2008 年 Sun 收购 MySQL AB;2010 年 Oracle 收购 Sun,MySQL 归 Oracle 所有。
    • 2010 年 MySQL 5.5 起,InnoDB 正式取代 MyISAM 成为默认存储引擎。
    • 社区分支:MariaDB(由 Monty 主导的社区分支)、Percona Server 等,在 InnoDB 基础上各自演进。

    主要版本里程碑:

    版本大致年份关键里程碑
    3.23 / 4.0 2000 前后 InnoDB 作为可选引擎集成(事务 / 行锁 / MVCC)
    5.0 2003–2005 InnoDB 成熟,引入视图、存储过程、触发器
    5.1 2008 分区表、事件调度、InnoDB 插件
    5.5 2010 InnoDB 成默认引擎;半同步复制;performance_schema
    5.6 2013 GTID、并行复制、InnoDB 全文索引、online DDL
    5.7 2015 JSON、生成列、多源复制、sys schema、查询缓存默认关闭
    8.0 2018 CTE/窗口函数、原子 DDL、不可见索引、移除查询缓存、数据字典入 InnoDB

    从 MyISAM 到 InnoDB 的更替,本质上是"事务与崩溃安全"成为刚需的结果;而 WAL、MVCC、两阶段提交这些原理,正是 InnoDB 能扛住核心业务的根基。

    小结

    一条 UPDATE 从应用发出,经连接池与 Server 层进入 InnoDB,先在 Buffer Pool 改内存、写 Undo 留退路、记 Redo 保崩溃安全,再经两阶段提交让 Binlog 与 Redo 对齐,最后由后台线程异步刷脏页落盘。WAL/ARIES 决定了"先写日志",LSN/Checkpoint 决定了"恢复从哪里开始",MVCC 与锁决定了并发下的可见性与正确性。

    这套机制并非一蹴而就,而是随 MySQL/InnoDB 从 MyISAM 到默认引擎的演进逐步定型,也是索引调优、参数配置与故障排查的共同基础。


    文章结束,喜欢就给个一键三连吧,你的肯定是我最大的动力,点赞上一千我就是脑瘫也出下章。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » MySQL 底层执行原理与实现历程
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!