[{"data":1,"prerenderedAt":72},["ShallowReactive",2],{"article-160":3},{"code":4,"msg":5,"data":6,"count":14},200,"查询成功",{"id":7,"title":8,"keywords":9,"description":10,"category_id":11,"content":12,"body_html":13,"thumb_up":14,"clicks":15,"sort":14,"remark":13,"status":16,"is_open":16,"is_deleted":14,"is_top":16,"is_recommend":16,"create_time":17,"update_time":18,"image_id":19,"url":13,"member_id":14,"cate_name":20,"prev":21,"next":24,"tags":27,"words":43,"read_time":44,"comments":14,"cover":45,"relevant":46},160,"一条 UPDATE 语句背后发生了什么？从 Buffer Pool 到两阶段提交","MySQL、InnoDB、redo log、两阶段提交","一条普通的 UPDATE 并不是找到磁盘中的某一行并直接改写。本文以 InnoDB 为主线，拆解数据页加载、undo log、Buffer Pool、redo log、binlog、脏页刷盘及两阶段提交，说明 MySQL 如何在性能、事务回滚、崩溃恢复与复制一致性之间取得平衡，并给出可直接用于面试的回答模板。",6,"```sql\nUPDATE account\nSET balance = balance - 100\nWHERE id = 42;\n```\n\n表面上，这条 SQL 只是把一行数据改掉。可 MySQL 真正执行它时，并不是先在磁盘上找到那一行，再原地改写。\n\n在默认存储引擎 InnoDB 中，这条语句会跨过 MySQL Server 层与存储引擎层，牵涉数据页、Buffer Pool、undo log、redo log、binlog、事务提交和后台刷盘。它们共同解决三个核心问题：**怎样改得快、怎样支持回滚、怎样在崩溃后保持数据一致。**\n\n\n> 本文讨论 MySQL 8.x、InnoDB、开启 binlog 的典型提交路径。为突出主线，省略锁竞争、索引维护、组提交、doublewrite buffer 等旁支细节。\n\n## 先给面试官一个简版答案\n\n一条 `UPDATE` 到达 MySQL 后，Server 层先完成解析、优化并调用 InnoDB。InnoDB 按页访问数据：目标页不在 Buffer Pool 时，先从磁盘读入内存；修改前生成 undo 信息，用于回滚和构造 MVCC 历史版本；随后在 Buffer Pool 中修改记录，使数据页成为脏页，并产生 redo 记录。\n\n如果开启 binlog，事务提交时会通过内部两阶段提交协调 redo log 与 binlog：先让 redo 进入 prepare，再写 binlog，最后提交 redo。这样即使中途崩溃，恢复时也能判断事务应该提交还是回滚。数据页本身不要求在提交时立即刷盘，之后可由后台线程批量写回；崩溃后则依靠已持久化的 redo 重放尚未落盘的修改。\n\n这段话已经覆盖了主干，但要真正答稳，还要知道每一步解决什么问题。\n\n## 1. 前置步骤：Server 层先决定“怎么执行”\n\n客户端发送 SQL 后，MySQL Server 层会处理连接、权限、语法解析和优化，生成执行计划，再由执行器调用 InnoDB 提供的接口访问记录。\n\n`WHERE id = 42` 是否能走主键或其他索引，决定了 InnoDB 如何定位目标记录。如果没有合适索引，可能需要扫描更多记录；但无论如何，InnoDB 与磁盘交互的基本单位都不是“某一行”，而是**数据页**。\n\n本文假设 `id` 能高效定位记录，并聚焦找到记录之后发生的事情。\n\n## 2. 为什么不直接修改磁盘\n\n如果每次修改都立即随机定位并改写磁盘数据页，高并发下会产生大量离散 I\u002FO，吞吐量很难接受。InnoDB 因此使用 Buffer Pool 缓存表和索引数据页。\n\n执行器找到目标记录时：\n\n1. 如果数据页已经在 Buffer Pool，直接使用内存中的页。\n2. 如果不在，InnoDB 先把对应磁盘页读入 Buffer Pool。\n3. 后续读取和修改都针对内存页进行。\n\n这样做并不意味着 `UPDATE` “只剩内存速度”——定位、加锁、日志写入和必要的磁盘同步仍然有成本——但它把数据文件的随机写从事务提交主路径中移开了。\n\n![Buffer Pool 与脏页刷盘](https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Fmarkdown\u002F20260715\u002Fb612a9dba22377d4a4393f639140d94d.png)\n\n## 3. 修改之前：undo log 负责“能够反悔”\n\nInnoDB 修改聚簇索引记录前，会生成相应的 undo 记录。更准确地说，undo log 保存的是**撤销本次修改所需的信息**，不一定是把旧行完整复制一份。\n\nundo log 主要承担两类职责：\n\n- **事务回滚**：事务执行失败或主动执行 `ROLLBACK` 时，InnoDB 根据 undo 信息撤销已经发生的修改。\n- **MVCC 一致性读**：其他事务需要读取较早版本时，可以沿版本链结合 undo 记录构造可见的历史版本。\n\n因此，undo log 回答的是：“如果这次修改不该生效，怎样恢复逻辑上的旧状态？”\n\n它和 redo log 的方向正好相反：undo 用来撤销，redo 用来重做。\n\n## 4. 真正修改发生在 Buffer Pool\n\nundo 信息准备好后，InnoDB 在 Buffer Pool 的数据页中修改目标记录。内存页与磁盘页此时不再一致，这个内存页就成为**脏页**。\n\n脏页不会因为“脏”就立即刷盘。InnoDB 会综合脏页比例、checkpoint 推进、可用页数量和系统负载等因素，在合适的时机把它写回数据文件。\n\n于是问题来了：事务已经提交，而脏页还没有落盘，此时服务器突然断电怎么办？\n\n答案是 redo log。\n\n## 5. redo log：先记录变化，再延后写数据页\n\nredo log 是 InnoDB 的崩溃恢复日志。数据页发生修改时，InnoDB 会生成描述相关页修改的 redo 记录，先进入 redo log buffer；事务提交时，再按配置把相应日志写入并同步到 redo log 文件。\n\n这体现了 WAL（Write-Ahead Logging，预写日志）思想：**先保证用于恢复的日志达到要求，再把随机的数据页写入延后。**\n\nredo 记录按日志序列持续追加，比每次提交都随机改写不同的数据页更适合磁盘和存储设备。若数据库在脏页刷盘前崩溃，InnoDB 重启时可以重放已持久化的 redo，把数据页恢复到正确状态。\n\n注意，redo log 不是“把 SQL 再执行一遍”，也不应简单等同为 binlog 那样的逻辑操作记录。它服务于 InnoDB 数据页的崩溃恢复。\n\n## 6. binlog：记录 Server 层的数据变更事件\n\nbinlog 属于 MySQL Server 层，不是 InnoDB 私有日志。它记录描述数据库变化的事件，常见用途包括：\n\n- 将源库的数据变更复制到副本；\n- 在恢复备份后重放后续事件，实现时间点恢复；\n- 为审计、订阅或增量处理提供变更来源。\n\n根据 `binlog_format`，事件可以采用 `STATEMENT`、`ROW` 或 `MIXED` 形式。因此，把 binlog 一概说成“原样记录 SQL”并不准确；在常见的 `ROW` 模式下，它记录的是行事件。\n\n![三种日志的职责对比](https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Fmarkdown\u002F20260715\u002F32a3894b4588ca8cbacfaeaca45ce5b2.png)\n\n| 日志 | 所属层 | 主要记录 | 主要用途 |\n| --- | --- | --- | --- |\n| undo log | InnoDB | 撤销修改、构造旧版本所需的信息 | 回滚、MVCC |\n| redo log | InnoDB | 与数据页变更和恢复相关的 redo 记录 | 崩溃恢复、延后数据页刷盘 |\n| binlog | Server 层 | 数据库变更事件 | 复制、归档、时间点恢复 |\n\n## 7. 为什么 redo log 和 binlog 需要两阶段提交\n\n同一个事务既要进入 InnoDB 的 redo log，又要进入 Server 层的 binlog。如果两个日志各写各的，就可能出现不一致：\n\n- redo 已提交、binlog 没有：主库恢复后有这笔数据，副本或时间点恢复却看不到。\n- binlog 已写、redo 未提交：副本可能执行了主库最终没有的数据变更。\n\n为协调这两个日志，MySQL 在典型路径中使用内部 XA 式两阶段提交。可以把主线简化为：\n\n1. **redo prepare**：InnoDB 写入该事务的 redo，并将事务置为 prepare 状态。\n2. **write binlog**：Server 层把该事务的 binlog 写入日志；是否每次都同步到存储设备受 `sync_binlog` 等配置影响。\n3. **redo commit**：Server 层通知 InnoDB 完成提交，在 redo 中记录 commit 状态。\n\n![两阶段提交与崩溃恢复](https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Fmarkdown\u002F20260715\u002Fa7fb9220305d97bf50898a624e94c1d2.png)\n\n### 不同崩溃点会怎样\n\n两阶段提交真正的价值，要结合崩溃点来看：\n\n- **redo prepare 之前崩溃**：事务没有形成可提交状态，恢复时按未提交事务处理。\n- **redo prepare 完成、binlog 尚未完整写入时崩溃**：恢复时找不到匹配的完整 binlog 事务，prepared 事务回滚。\n- **binlog 已完整写入、redo commit 之前崩溃**：恢复时可以通过事务标识确认 binlog 中存在完整事务，随后提交对应的 prepared 事务。\n- **redo commit 之后崩溃**：事务已经提交，按提交状态恢复。\n\n所以，“redo 处于 prepare，重启后一律回滚”是错误的。恢复结果还要看 binlog 中是否存在完整、匹配的事务。\n\n## 8. 把整条执行链路串起来\n\n![UPDATE 总体执行流程](https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Fmarkdown\u002F20260715\u002F19f7ecd97eab9bf87787f055424f2fcb.png)\n\n以自动提交模式下的一条普通 `UPDATE` 为例，可以把主线整理为：\n\n1. Server 层解析 SQL、选择执行计划并调用 InnoDB。\n2. InnoDB 通过索引定位记录，必要时把数据页读入 Buffer Pool。\n3. 对目标记录加必要的锁，并生成 undo 信息。\n4. 修改 Buffer Pool 中的记录，数据页成为脏页。\n5. 生成 redo 记录，先进入 redo log buffer。\n6. 提交阶段让 redo 进入 prepare。\n7. Server 层写入 binlog。\n8. InnoDB 完成 redo commit。\n9. 满足当前持久化配置要求后，Server 层向客户端返回提交成功。\n10. 后台线程在之后把脏页刷新到数据文件；如果刷盘前崩溃，则重启时用 redo 恢复。\n\n如果关闭自动提交并显式开启事务，那么 `UPDATE` 语句执行结束并不等于事务已经提交。真正的提交链路发生在后续 `COMMIT` 时。这是面试回答中经常被忽略的边界。\n\n## 9. “返回成功就绝不丢数据”需要配置前提\n\n只说“有 redo log，所以提交后一定不丢”并不严谨。可靠性还取决于日志何时真正同步到持久化设备。\n\n两个关键参数是：\n\n- `innodb_flush_log_at_trx_commit=1`：每次事务提交时写并刷 redo log，是完整 ACID 持久性的默认要求。\n- `sync_binlog=1`：每个 binlog 提交组都同步到磁盘，是 binlog 最安全的设置。\n\n官方文档建议，在使用 InnoDB 事务并开启 binlog、希望获得尽可能高的持久性和一致性时，同时使用这两个值。若为了性能改成更宽松的配置，数据库或操作系统崩溃时可能丢失最近已经返回成功的事务。\n\n此外，存储硬件和操作系统必须正确兑现 flush 请求；如果设备错误地报告“已落盘”，即使参数设置正确，也无法做绝对保证。\n\n## 10. 三个常见误区\n\n### 误区一：UPDATE 就是直接改磁盘上的一行\n\nInnoDB 按页进行缓存和 I\u002FO，通常先修改 Buffer Pool 中的数据页，再异步刷回数据文件。\n\n### 误区二：修改完内存就立即返回成功\n\n事务提交前仍要完成日志提交协议，并满足当前持久化配置要求。显式事务中，单条 `UPDATE` 执行完甚至还没有进入最终提交。\n\n### 误区三：redo log 和 binlog 是同一种日志\n\nredo log 属于 InnoDB，服务于数据页崩溃恢复；binlog 属于 Server 层，服务于复制和恢复。两者职责、记录形式和生命周期都不同。\n\n## 11. 一份可以直接复述的面试回答\n\n> InnoDB 执行 UPDATE 时，不会直接随机改写磁盘中的某一行，而是先通过索引定位记录，并确保目标数据页位于 Buffer Pool。修改前生成 undo 信息，支持事务回滚和 MVCC；随后修改内存页，使其成为脏页，同时产生用于崩溃恢复的 redo 记录。\n>\n> 如果开启 binlog，提交阶段会通过两阶段提交协调 redo log 与 binlog：先 redo prepare，再写 binlog，最后 redo commit。这样即使在任一阶段崩溃，恢复时也能结合 redo 状态和完整 binlog 判断事务应提交还是回滚。数据页可以稍后由后台线程刷盘；如果刷盘前宕机，则通过 redo 重放恢复。最终持久性还取决于 `innodb_flush_log_at_trx_commit`、`sync_binlog` 以及底层存储是否正确执行 flush。\n\n如果面试官继续追问，可以补充两点：显式事务要到 `COMMIT` 才真正提交；redo 处于 prepare 时不一定回滚，还要检查 binlog 中是否存在完整事务。\n\n## 总结\n\n一条 `UPDATE` 的核心不是“把一行改掉”，而是让多个机制围绕同一次修改达成协作：\n\n- Buffer Pool 把数据文件随机写移出事务主路径；\n- undo log 让事务能够撤销，并支持一致性读；\n- redo log 让尚未刷盘的数据页可以在崩溃后恢复；\n- binlog 保存可用于复制和恢复的数据变更事件；\n- 两阶段提交让 redo log 与 binlog 对同一事务作出一致判断。\n\n真正理解这条链路之后，Buffer Pool、WAL、MVCC、崩溃恢复和复制一致性就不再是几个孤立名词，而会成为一套完整的事务系统。\n\n## 参考资料\n\n- [MySQL 8.0 Reference Manual：InnoDB Buffer Pool](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Finnodb-buffer-pool.html)\n- [MySQL 8.0 Reference Manual：Redo Log](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Finnodb-redo-log.html)\n- [MySQL 8.0 Reference Manual：Undo Logs](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Finnodb-undo-logs.html)\n- [MySQL 8.0 Reference Manual：The Binary Log](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Fbinary-log.html)\n- [MySQL 8.0 Reference Manual：InnoDB Startup Options and System Variables](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Finnodb-parameters.html)\n- [MySQL 8.0 Reference Manual：Binary Logging Options and Variables](https:\u002F\u002Fdocs.oracle.com\u002Fcd\u002FE17952_01\u002Fmysql-8.0-en\u002Freplication-options-binary-log.html)\n",null,0,3,1,"2026-07-15 21:59:29","2026-07-15 21:59:30",263,"Mysql",{"id":22,"title":23},159,"百万在线 IM 已读未读怎么设计？",{"id":25,"title":26},161,"Redis 为什么这么快？单线程只是答案的一部分",[28,31,34,37,40],{"id":29,"name":30},26,"MySQL",{"id":32,"name":33},166,"InnoDB",{"id":35,"name":36},167,"后端面试",{"id":38,"name":39},168,"数据库原理",{"id":41,"name":42},169,"事务日志",5183,12,"https:\u002F\u002Ftp.myong.top\u002Fstorage\u002Farticle\u002F20260715\u002F7656bdd1b2c75775d30555686314b497.png",[47,52,57,62,67],{"id":48,"title":49,"create_time":50,"description":51},90,"Redis常见使用场景","2020-06-17 16:09:42","Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库，并提供多种语言的API。本篇文章，主要介绍利用PHP使用Redis，主要的应用场景。",{"id":53,"title":54,"create_time":55,"description":56},94,"客户端连接MySQL8提示 caching-sha2-password 问题","2020-10-19 10:55:26","在安装mysql8的时候如果选择了密码加密，之后用客户端连接比如navicate，会提示客户端连接caching-sha2-password,是由于客户端不支持这种插件，可以通过如下方式进行修改：",{"id":58,"title":59,"create_time":60,"description":61},87,"MySQL中InnoDB和MyISAM区别","2020-06-12 17:11:21","InnoDB具有事务，支持4个事务隔离级别，回滚，崩溃修复能力和多版本并发的事务安全，包括ACID.如果应用中需要执行大量的INSERT或UPDATE操作，则应该使用InnoDB,这样可以提高多用户并发操作的性能。MyISAM管理非事务表，提供高速存储和检索，以及全文搜索能力，如果应用中需要执行大量的SELECT查询，那么MyISAM是更好的选择。",{"id":63,"title":64,"create_time":65,"description":66},88,"MySQL实现循环插入千万级数据","2020-06-12 17:56:22","对于一些数据量较大的系统，数据库面临的问题除了查询效率低下，还有就是数据入库时间长。特别像报表系统，可能每天花费在数据导入上的时间就会长达几个小时之久。因此，优化数据库插入性能是很有意义的。",{"id":68,"title":69,"create_time":70,"description":71},86,"MySQL查询表结构命令","2020-06-09 11:30:00","###### MySQL查询表结构命令\n\n###### 1、查询表结构\n\n主要显示字段类型主键是否允许为空等\n\n```mysql\nDESC 表名;\n```\n\n结果显示\n\n| Field | Type         | Null | Key  | Default | Extra          |\n| :---- | ------------ | ---- | ---- | ------- | -------------- |\n| id    | int(11)      | NO   | P",1785081778621]