前言

        在开发中,我们会经常与 MySQL 打交道。一条简单的 UPDATE accounts SET balance = balance - 100 WHERE user = 'A' 语句,我们在客户端轻轻一点就执行完了。但你有没有想过,这条 SQL 语句在 MySQL 内部究竟经历了怎样的旅程?为什么即使在服务器突然断电的极端情况下,已提交的事务依然能保证数据不丢失?为什么要有 redo log 和 binlog 两套日志系统?

        接下来,我们解析从客户端发出请求到数据最终落盘的全过程,揭开数据库系统最精妙的设计。

1 MySQL整体架构

        要理解 SQL 的执行过程,首先要搞清楚 MySQL 的体系结构。MySQL 整体可以分为三层:连接层、Server 层和存储引擎层。

1.1 三层架构

层级 组件 功能描述
连接层 连接器、连接池 处理客户端连接请求,进行用户认证和授权,管理和复用连接
Server层 SQL接口、解析器、优化器、执行器 SQL解析、优化、执行,跨存储引擎的服务功能
存储引擎层 InnoDB、MyISAM、Memory等 负责数据的实际存储和读取,支持插件式架构

        Server 层是 MySQL 的核心中枢,涵盖连接器、查询缓存(MySQL 8.0 已移除)、分析器、优化器和执行器,以及所有的内置函数(如日期、时间、数学等),所有跨存储引擎的功能(如存储过程、触发器、视图)都在这一层实现。

        存储引擎层负责数据的存储和提取,采用插件式的架构模式,最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为默认存储引擎。

1.2 核心组件速览

2 一条UPDATE语句的完整旅程

        假设我们执行这样一条更新语句:

UPDATE accounts SET balance = balance - 100 WHERE user = 'A';

2.1 Step1:连接器——建立连接与权限校验

        客户端输入 mysql -uroot -proot 命令后,连接器首先完成经典的 TCP 三次握手,与 MySQL Server 建立 TCP 连接。随后,连接器对请求进行权限校验:

  • 如果用户名或密码错误,会收到 错误提示,客户端程序结束执行;

  • 校验通过后,连接器会从权限表将当前用户的所有权限查询并缓存起来,权限缓存的生命周期一直持续到该连接关闭。

注:权限修改后,已存在的连接不会立即生效——新权限只对新建的连接生效。

        此外,还需要注意长连接带来的内存问题。MySQL 在执行过程中临时使用的内存是管理在连接对象里的,这些资源直到连接断开才会释放。如果长连接累积过多,可能导致内存占用过大,甚至被系统 OOM Kill,建议定期断开长连接。

2.2 Step2:分析器——拆解 SQL,检查语法

        连接建立后,SQL 语句被传递给分析器。分析器的工作分为两个阶段:

                1. 词法分析:将 SQL 字符串拆解成一个个词元,识别出 SQL 的关键字(如 UPDATE、WHERE)、表名、字段名和值等信息。   

                2.     语法分析:根据语法规则检查 SQL 语句的结构是否正确。如果 SQL 写错了,比如把 WHERE 写成了 WHER,分析器就会报错并终止执行。

2.3 Step3:优化器——选择最优执行路径

        优化器是 SQL 执行效率的关键。它的核心任务是:在众多可能的执行方案中,选择一条最优路径。

        具体来说,优化器会考虑以下因素:

                1. 是否使用索引,以及使用哪个索引

                2. 多表关联的连接顺序

                3. 根据表的统计信息估算各方案的执行代价

        最终生成一个"执行计划",供后续的执行器使用。

2.4 Step4:执行器——调用存储引擎接口

        执行器是整个流程的"操作手"。拿到优化器生成的执行计划后,执行器会循环调用存储引擎提供的接口来逐行处理数据:

                1. 对于查询操作,执行器调用存储引擎的"读取下一行"接口,每次取一行数据,判断是否符合查询条件

                2. 对于更新操作(如 UPDATE),执行器先找到符合条件的数据行,调用存储引擎的更新接口执行修改,然后将更新操作写入 redo log 和 binlog 中。

3 两阶段提交:数据一致性的“守护神”

3.1 为什么要两阶段提交?

        在 MySQL 内部,事务的提交涉及两个独立的日志系统——redo log(InnoDB 层)和 binlog(Server 层),如果没有协调机制,可能会出现"半成功"的灾难场景。 

场景 问题表现 后果
Redo log 先写成功,Binlog 未写就宕机 主库重启后通过 Redo log 恢复数据,但 Binlog 缺失该事务 主从复制后从库少了这笔记录,主从不一致
Binlog 先写成功,Redo log 未写就宕机 Binlog 中有该事务记录,但主库 Redo log 未提交,崩溃恢复后事务无效 主库回滚但从库执行了该事务,从库凭空多出数据

        两阶段提交就是为了解决这一问题而引入的。它将一个事务的提交过程拆分为 Prepare 阶段和 Commit 阶段,并用一个全局唯一的事务 ID将 redo log 和 binlog 拴在一起,确保两份日志要么都成功,要么都失败(事务要么全做,要么全不做)。

3.2 两阶段提交的完整过程

        具体步骤:

                Prepare阶段:

                        1. InnoDB 将此次修改写入 redo log

                        2. redo log 的状态被标记为 prepare

                        3. redo log 根据参数设置决定是否刷盘

                Commit阶段:

                        1. Server 层将 XID 写入 binlog

                        2. binlog 根据参数决定是否刷盘

                        3. 调用 InnoDB 引擎的提交接口,将 redo log 的状态从 prepare 改为 commit

3.3 崩溃恢复机制

        两阶段提交的精妙之处体现在崩溃恢复时。MySQL 在重启后,根据以下逻辑判断事务是否需要提交。

Redo log 状态 Binlog 状态 恢复行为 原因
有 commit 标识 有完整记录 提交 事务已完成提交
仅有完整 prepare 有完整记录 提交 Binlog 已写,可用于主从同步
仅有完整 prepare 缺失/不完整 回滚 Binlog 未写,该事务不应生效

        崩溃恢复时的判断逻辑:如果 redo log 里面的事务已经有了 commit 标识,直接提交;如果只有完整的 prepare,则进一步判断对应的事务 binlog 是否存在且完整——若完整则提交,否则回滚。

        这个设计确保了无论崩溃发生在两阶段提交的哪个环节,最终两份日志的状态都是一致的。在时刻 A(redo log prepare 后、写入 binlog 之前)崩溃,发现 redo log 没有 commit,事务回滚;在时刻 B(写入 binlog 后、redo log commit 之前)崩溃,发现 binlog 完整,事务正常提交。

总结

        回到开头的问题:为什么断电后已提交的事务不会丢失?因为 Redo log 在事务提交时已经写入了磁盘。为什么主从不会出现不一致?因为两阶段提交确保了 Redo log 和 Binlog 在逻辑上"荣辱与共"。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐