接好运鸭 发表于 7 小时前

MySQL 底层拆解:为什么付款完成后数据库被 kill‑9 杀掉,重启之后数据还在?

<h2>前言</h2><div class="quote"><blockquote><p><font size="4">后端开发经常会遇到一个很反直觉的现象:一笔订单付款完成,业务返回 “付款成功”,此时直接用<code>kill‑9</code>强制杀掉 MySQL 进程。去磁盘上直接看数据文件,这笔新增订单记录看不到;但重启 MySQL 之后,刚刚那笔付款记录又完整回来了。</font></p><p><font size="4">很多人会疑惑:明明磁盘数据文件没有写入,为什么宕机重启数据不会丢?本文结合 MySQL 8.4,从 SQL 执行流程、B + 树索引、缓冲池、redo log、undo log 三大日志讲清背后原理,同时结合实测案例,搞懂索引失效、回表、WAL 预写日志这些高频面试知识点MySQL。</font></p><blockquote>
<p><font size="4">概念释义
<strong>kill‑9</strong>:操作系统强制终止进程,不给数据库执行优雅关闭、刷脏页的机会。
<strong>Buffer Pool 缓冲池</strong>:InnoDB 内存缓冲区,缓存数据页与索引页,减少磁盘 IO。
<strong>脏页</strong>:内存中被修改过,但还没有同步写入磁盘数据文件的数据页。
<strong>WAL 预写日志(Write‑Ahead Logging)</strong>:修改数据前,先把变更记录写入 redo log 持久化磁盘,再异步刷新数据页,保证宕机可恢复。
<strong>redo log 重做日志</strong>:记录数据修改动作,用于崩溃恢复,保证事务持久性。
<strong>undo log 回滚日志</strong>:记录修改前数据快照,用于事务回滚、MVCC 多版本控制。
<strong>binlog 二进制日志</strong>:Server 层日志,记录行变更,用于主从复制、时间点数据恢复。
<strong>聚簇索引</strong>:InnoDB 主键索引,B + 树叶子节点存放完整行数据;二级索引叶子节点只保存主键,需要<strong>回表</strong>读取完整行。</font></p></blockquote></blockquote></div><p><br></p><blockquote>
</blockquote><h2>一、MySQL 整体架构:Server 层 + InnoDB 存储引擎层</h2><p>MySQL 整体分为两层:</p><ol>
<li><strong>Server 层</strong>:连接器、解析器、优化器、执行器。负责连接管理、SQL 语法解析、生成执行计划、调用存储引擎接口。MySQL8.0 已经彻底移除旧版查询缓存。</li>
<li><strong>存储引擎层(InnoDB)</strong>:真正负责数据读写、事务、日志、索引。InnoDB 是 MySQL5.5 之后的默认存储引擎腾讯云。</li>
</ol><p><strong>一条 SQL 完整执行链路:</strong></p><ol>
<li>连接器:建立 TCP 连接,校验账号密码权限;后端服务一般使用连接池复用连接,避免频繁创建销毁线程。</li>
<li>解析器:词法、语法解析,校验表名列名、权限,生成抽象语法树。</li>
<li>优化器:评估多条执行路径成本,选择索引,生成最优执行计划。</li>
<li>执行器:调用 InnoDB 引擎接口,读取、过滤数据。</li>
<li>InnoDB 层:访问 Buffer Pool 缓冲池;命中内存直接读取,未命中则从磁盘加载数据页;执行读写逻辑,处理 undo、redo 日志。</li>
</ol><blockquote>
<p>MySQL 每一条新连接会分配独立工作线程,不是进程,线程开销更小。</p>
</blockquote><h2>二、B + 树索引、回表与索引失效实战案例</h2><p>InnoDB 数据以<strong>16KB 为一页</strong>单位存储,所有表都是 B + 树结构。主键是聚簇索引,叶子节点存放完整行;普通二级索引叶子只存主键值,拿到主键后,再到聚簇索引读取完整行,这个过程叫做<strong>回表</strong>CSDN博...。</p><p>以一千万行订单表实测:</p><ul>
<li><strong>全表扫描</strong>:要读取约 55500 个数据页,耗时 4 秒。</li>
<li><strong>走 user_id 二级索引</strong>:索引树 3 层,先从二级索引拿到订单主键,再回表读取完整记录,一共只访问 16 个页,耗时仅 3 毫秒。</li>
</ul><blockquote>
<p>索引不是没有代价:建立索引消耗磁盘空间,写入更新数据,所有相关索引都需要同步维护,会增加写开销。</p>
</blockquote><h3>一个经典索引失效坑</h3><p>两张查询逻辑返回结果完全一致,性能天差地别:</p><pre style="font-family: Consolas, Monaco, &quot;Courier New&quot;, monospace;"><code>-- ✅可以走create_time索引,65ms
select * from orders where create_time &gt;= '2026‑03‑05 00:00:00' and create_time &lt; '2026‑03‑06 00:00:00';

-- ❌索引列使用DATE()函数,索引失效,触发全表扫描,耗时3.7秒
select * from orders where DATE(create_time) = '2026‑03‑05';
</code></pre><p>原因:B + 树索引保存的是<strong>字段原始值</strong>,在索引列上做函数运算,数据库无法利用索引有序性,只能逐行计算过滤,导致索引失效。MySQL8.0 支持函数索引,但业务上优先改写 where 条件更稳妥。</p><h3>Buffer Pool 缓冲池带来的速度差异</h3><p>同样一条索引查询,第一次执行 3ms,第二次只需要 0.2ms。
原因:第一次需要把索引、数据页从磁盘加载进 Buffer Pool;第二次全部命中内存,不需要磁盘 IO。生产环境数据库服务器,通常会把物理内存的 60%‑80% 分配给 Buffer Pool。</p><h2>三、核心问题:kill‑9 强杀 MySQL,付款记录为什么重启又回来了?</h2><p>业务收到 “付款成功” 响应,立刻<code>kill‑9</code>强制杀死 MySQL 进程。此时内存里修改的数据页(脏页)来不及刷新到磁盘上的 ibd 数据文件,直接看磁盘文件看不到这条新订单。但是<strong>redo log 已经提前持久化到磁盘</strong>。</p><p>这就是 InnoDB 核心机制 <strong>WAL 预写日志(Write‑Ahead Logging)</strong>:</p><blockquote>
<p>事务提交的顺序:先写 redo log 落盘,脏页数据页后续后台异步刷盘,不需要等待数据页写入磁盘。</p>
</blockquote><p>完整流程:</p><ol>
<li>执行 update /insert 付款记录;</li>
<li>写入 undo log,保存修改前数据,用于事务回滚;</li>
<li>修改 Buffer Pool 内存里的数据页,变成脏页;</li>
<li>把本次变更操作记录写入 redo log buffer;</li>
<li><strong>事务提交,redo log 持久化 fsync 到磁盘</strong>,此时给业务返回 “付款成功”;</li>
<li>脏页不会立刻写磁盘,后台 checkpoint 线程慢慢把脏页刷新到 ibd 数据文件。</li>
</ol><p>当<code>kill‑9</code>宕机:内存全部丢失,但 redo log 文件已经保存在磁盘。重启 MySQL,InnoDB 执行<strong>崩溃恢复 Crash Recovery</strong>,扫描 redo log,把已经提交、但脏页没有刷盘的变更重新重演一遍;未提交的事务依靠 undo log 回滚。于是那条付款记录就回来了MySQL。</p><blockquote>
<p>关键点:</p>
<ol>
<li>业务收到成功,代表 redo log 已经落盘,数据具备持久性;脏页刷盘是后台异步动作。</li>
<li>undo log 负责回滚未提交事务;redo log 负责崩溃之后重做已提交的变更;binlog 是 Server 层日志,用于主从复制、时间点恢复,三者各司其职。</li>
</ol>
</blockquote><h2>四、三大日志简单区分</h2>
























<p>表格</p><table><thead><tr><th>日志</th><th>归属</th><th>核心作用</th></tr></thead><tbody><tr><td>redo log 重做日志</td><td>InnoDB 存储引擎</td><td>WAL 崩溃恢复,保证已提交事务宕机不丢失</td></tr><tr><td>undo log 回滚日志</td><td>InnoDB 存储引擎</td><td>事务回滚、MVCC 多版本并发控制</td></tr><tr><td>binlog 二进制日志</td><td>MySQL Server 层</td><td>主从同步、基于时间点数据恢复</td></tr></tbody></table><h2>五、常见认知误区澄清</h2><ol>
<li>误区:事务提交,数据就立刻写入磁盘 ibd 文件</li>
</ol>
<blockquote>
<p>真相:提交保证 redo log 落盘;脏页是后台异步刷入数据文件,两者不同步。kill‑9 杀掉进程,ibd 文件可以看不到新数据,重启靠 redo log 恢复。</p>
</blockquote>
<ol start="2">
<li>误区:建立索引,where 条件就一定能用上索引</li>
</ol>
<blockquote>
<p>真相:索引列做函数运算、隐式类型转换、like 前缀通配符,都会造成索引失效,触发全表扫描。</p>
</blockquote>
<ol start="3">
<li>误区:Buffer Pool 越大,磁盘读写就完全消失</li>
</ol>
<blockquote>
<p>真相:冷库、大量新数据,依然会产生磁盘 IO;缓冲池只是缓存热点页。</p>
</blockquote>
<ol start="4">
<li>误区:binlog、redo log 是同一个东西</li>
</ol>
<blockquote>
<p>真相:binlog 属于 Server 层记录逻辑行变更;redo log 是 InnoDB 物理层面页变更日志,用途完全不一样。</p>
</blockquote>
<ol start="5">
<li>误区:回表就是 bug,应该尽量全部避免</li>
</ol>
<blockquote>
<p>真相:回表是 InnoDB 二级索引正常机制;可以通过覆盖索引消除回表,但覆盖索引会增加索引存储开销。</p>
</blockquote><h2>结语</h2><p>很多后端开发者天天写 SQL,却不了解 MySQL 内部发生了什么。
一条 SQL 从客户端发出,经历连接器、解析器、优化器、执行器,再进入 InnoDB,依靠 B + 树索引、Buffer Pool 缓冲池提升查询性能;依靠 WAL 预写日志机制,<strong>redo log 保证宕机数据不丢,undo log 保证可以回滚,binlog 负责复制与数据恢复</strong>。</p><p><code>kill‑9</code>杀掉 MySQL,磁盘数据文件看不到刚刚提交的数据,重启之后记录重现,正是 WAL 预写日志的典型体现。同时也要注意索引失效、回表等性能坑,写 SQL 的时候,多使用<code>explain</code>查看执行计划,确认索引是否真正生效。</p><p></p>
页: [1]
查看完整版本: MySQL 底层拆解:为什么付款完成后数据库被 kill‑9 杀掉,重启之后数据还在?