做后端这些年,MySQL是绕不开的坎,而InnoDB底层原理和MySQL日志机制,又是坎中坎。很多人增删改查玩得飞起,一聊到事务隔离、崩溃恢复、主从复制就露怯。这篇我想把这两块内容串起来讲透:从一条UPDATE语句如何在InnoDB内部流转,到redo log、undo log、binlog为什么能配合着保证数据不丢、状态一致,再到实际生产中怎么调参数、怎么基于binlog做恢复。内容会偏原理,也会带上我在实际排查中踩过的坑和结论,适合正在啃MySQL内核资料、或者遇到了“丢数据”“日志暴涨”这类问题的开发者和DBA参考。
1. 先看一条UPDATE语句在InnoDB里是怎么走的
1.1 从客户端到存储引擎的完整链路
很多文章一上来就甩出B+树、索引结构,但我觉得最好的切入点是拿着一句真实SQL走一遍全流程。假设表里有一行id=100, balance=500,我们执行:
UPDATE user SET balance = balance + 100 WHERE id = 100;这条语句到达MySQL Server层后,要先经过解析器做语法分析,生成语法树;再经过优化器决定走主键索引还是二级索引,最终生成执行计划;然后交由执行器调用InnoDB的接口,一层层下推到存储引擎。InnoDB拿到主键id=100后,会先去缓冲池(buffer pool)里找该页在不在内存。如果在,直接对这页里的行做修改;如果不在,就从磁盘把对应数据页读到缓冲池中,再修改。这里有个关键点:只要事务没提交,这个修改就只存在于内存页中,并没有落盘。修改完后,执行器会记录一条binlog(在Server层),而且InnoDB内部会同时生成对应的redo log和undo log。等到提交事务时,才触发真正的日志落盘和页刷盘。
这个流程看着简单,但里面暗含了很多设计选择:为什么不让执行器直接改磁盘文件?为什么日志要分三层?答案都指向两个词:“随机写”和“顺序写”的代价差异。修改一个页需要定位到磁盘上的某个随机位置,这是毫秒级的慢操作;而追加一条日志只需要在文件末尾顺序写,即使每秒几千条事务,也能轻松扛住。InnoDB把“先改内存、后记日志”的组合叫做WAL(Write-Ahead Logging),也就是日志先行。日志落盘了,事务就算成功一半了,数据页可以留在内存里慢慢刷回磁盘。
1.2 为什么先写日志再改数据页
刚开始接触WAL时,我的直觉是应该先改数据再记日志,否则崩溃了怎么办。后来想明白一个朴素的道理:日志描述的是“我要做什么”,数据页是“做成什么样”,如果先改数据后记日志,崩溃时你无法分辨数据页里哪些是新状态,哪些是半成品。反过来,只要日志是完整且持久化的,那么无论数据页处于什么状态,都能依据日志把事务重放或者回滚。
举个例子:事务A要给balance加100,InnoDB生成的redo log内容是“把id=100这行的balance从500改成600”。这句话被写进日志文件后,就算落盘时数据页还是旧值500,崩溃恢复时MySQL照样能根据redo log把它重放成600。如果redo log还没落盘,事务就崩溃了,那这个事务本来也没提交,用户不会认为它生效过,自然也不需要恢复。这套“先日志、后数据”的机制,是整个崩溃恢复体系的地基。
理解这个链路后,我们再深入InnoDB的存储结构。因为日志说来说去都是为了保护那些复杂的页结构,不知道页长什么样,就很难理解为什么恢复过程需要那么多细节。
2. InnoDB底层存储结构:B+树、页与表空间
2.1 行记录与页的物理组织
InnoDB把磁盘空间划分为表空间(tablespace)、段(segment)、区(extent)、页(page)四级。绝大多数情况下,一个普通表对应一个独立表空间文件(ibd文件),文件里按区、页来分配空间。每页默认16KB,页内又按行格式存放多条记录。行格式常见的是DYNAMIC和COMPACT,两者的差异主要体现在变长字段(如VARCHAR、TEXT)是不是“溢出存储”。当一行数据太长,塞不进一个页里时,InnoDB会把部分字段存到单独的溢出页,原页只保留一个指向溢出页的指针。
页的内部结构也很讲究:页头记录页的编号、上一页下一页指针、页类型等信息;页体是用户记录;页尾则有校验和。数据页之间通过双向链表连在一起,目的是方便范围扫描时顺序遍历。但光有链表不够,还得有索引来快速定位到底在哪一页。这就引出了B+树。
B+树是InnoDB索引的基石,每个索引都是一棵B+树。树的叶子节点存放完整的行记录,非叶子节点只存放索引键和指向子页的指针。为什么用B+树而不是二叉树或哈希表?因为InnoDB面对的是磁盘存储,访问一个节点往往就是一次磁盘I/O。B+树的扇出很高,三层树就能支撑上亿行数据的索引查找,把绝大多数查询的磁盘I/O次数控制在3次以内,这个成本对数据库来说太值了。
2.2 聚集索引与二级索引如何协作
InnoDB的表就是按主键聚集的,也就是说主键索引的叶子节点直接存整行数据。这带来一个重要的物理特性:表数据本身就是主键B+树。你没建主键时,InnoDB会自动生成一个隐式的ROWID作为聚集索引键;如果你定义了主键,主键就是聚集索引。正因如此,InnoDB特别推荐使用自增整数或趋势递增的值做主键,避免随机插入导致页分裂和索引频繁重排。
二级索引(普通索引)的叶子节点不存整行数据,只存索引列的值和主键值。查询时如果SELECT的列不在二级索引里,就要走“回表”:先在二级索引B+树中找到主键值,再到聚集索引B+树中取整行。为了减少回表,可以把常查的列塞进索引末尾形成覆盖索引。这里有个实操心得:我在建索引时,会先看SQL的WHERE、ORDER BY、GROUP BY涉及哪些列,优先保证等值查询和排序字段能被索引直接覆盖,性能优化往往从这一步开始。
2.3 表空间设计带来的存储管理优势
表空间分为系统表空间、独立表空间、通用表空间和临时表空间。系统表空间里存着数据字典、双写缓冲等关键元信息;独立表空间是innodb_file_per_table=ON时每个表一个ibd文件。我强烈建议生产环境开启innodb_file_per_table,因为它让单表备份、删除、空间回收都更灵活。如果所有表挤在同一个共享表空间里,一张大表删掉后,这个表空间里的空闲碎片很难直接归还给操作系统,容易导致磁盘空间虚高。
InnoDB还有一个常用的结构叫“区”。一个区固定包含64个页,也就是1MB。为什么要有区?因为InnoDB在为表分配空间时,以区为单位向后端申请,这样能提高顺序读写的效率。当一张表的数据量比较小,不足以占满一个区时,采用“碎片页”的方式分配;当数据量超过一定阈值,才转向整区分配。这个细节平时感知不强,但当你观察information_schema.INNODB_TABLESPACES里的碎片率时,就能理解为什么需要定期整理表了。
3. 缓冲池:InnoDB性能的心脏
3.1 buffer pool与LRU算法的细节
InnoDB读写的真正主战场是内存缓冲池。缓冲池默认大小一般建议设为物理内存的60%~70%,里面缓存了三类数据:数据页、索引页、undo页,还有一部分用于锁信息和自适应哈希索引。如果你在SHOW ENGINE INNODB STATUS里看到Buffer pool hit rate低于99%,说明内存命中率偏低,热点页经常被淘汰,此时加内存或优化查询比盲目调SQL更有效。
缓冲池内部使用改良版LRU算法管理页,这与操作系统的LRU有个显著区别:InnoDB把链表分成New区和Old区,默认比例为5:37(参数innodb_old_blocks_pct可调,默认37)。新读入的页先放到Old区头部,只有当再次被访问时才会被晋升到New区。这么做是为了防“扫描污染”:一个全表扫描会把海量冷页读入缓冲池,如果没有Old区隔离,这些冷页会把真正的热点页瞬间挤出去,造成大量后续磁盘I/O。
我在实际调优中,对需要频繁扫描的大报表,会临时把innodb_old_blocks_time调大,比如设置成1000毫秒,让第一次读入的页在Old区停留超过指定时间,再次访问才晋升。这样能减少报表扫描对在线业务缓存的影响,代价是报表本身的速度可能略降。权衡下来,对在线交易系统很有效。
3.2 刷脏路径与Checkpoint机制
内存里被修改但还没写回磁盘的页叫“脏页”。脏页不能一直攒着,否则崩溃恢复时redo log会变得极其庞大,恢复耗时不可接受。InnoDB通过page cleaner线程按一定算法把脏页批量刷到磁盘。刷脏不是随机的,它优先刷“最旧”的脏页,依靠的就是Checkpoint机制。
Checkpoint本质上是一个LSN(Log Sequence Number)位置,表示“这个LSN之前的数据页已经刷盘完成”。恢复时只需要从Checkpoint之后做重放,所以Checkpoint越靠后,崩溃恢复时间越短。InnoDB会周期性触发模糊检查点,不是把全部脏页都刷完才推进,而是维护一个flushed_lsn和last_checkpoint_lsn,让二者保持合理差距。刷盘策略也受参数影响:
innodb_max_dirty_pages_pct = 75 innodb_io_capacity = 200 innodb_flush_neighbors = 0innodb_io_capacity告诉InnoDB你的磁盘每秒能处理多少次I/O,SSD可以设高一点,比如2000;机械盘设低一点,避免刷脏把正常读写堵死。innodb_flush_neighbors在SSD上建议设0,因为相邻页刷盘在HDD上能减少磁道移动,在SSD上反而增加无谓I/O。这些参数没有银弹,我每次都会结合iostat观察await指标来定。
3.3 Change Buffer与自适应哈希索引的实际收益
Change Buffer(变更缓冲)是我觉得InnoDB最容易被低估的机制。它专门缓存对二级索引页的修改(INSERT、UPDATE、DELETE),当目标索引页不在缓冲池时,不直接读取磁盘修改,而是先记录变更操作,等下次该页被读到或定期合并时再应用。因为二级索引通常是非唯一的,修改的页往往随机分布,每次都要磁盘I/O读页太亏,通过Change Buffer把随机写变成顺序写,性能提升非常可观。
但Change Buffer也不是越大越好。innodb_change_buffer_max_size默认占缓冲池25%,如果你的表很少写或者二级索引很多,可以保持默认;如果写入压力大、二级索引特别多,可以适当调高。注意:唯一索引无法使用Change Buffer,因为唯一性检查必须在写入时立即读取索引页来确认,所以生产环境中业务允许的话,能用普通索引就别用唯一索引,性能差异在某些场景下能到一倍以上。
自适应哈希索引则是InnoDB为热点等值查询自动建立的内存哈希索引。它不需要用户干预,当某个索引页被反复以等值条件访问时,InnoDB会为该页构建一个哈希索引,把O(logN)的B+树查找变成O(1)的哈希查找。但有个陷阱:在连接数多、并发量高的场景下,自适应哈希索引的全局锁可能成为瓶颈,所以MySQL 8.0提供了参数innodb_adaptive_hash_index_parts把哈希索引分区。遇到高并发负载时,我们要检查SHOW ENGINE INNODB STATUS里有没有类似btr_search_*的锁争用信息,必要时直接关掉它。
4. MySQL日志机制拆解:redo、undo、binlog
4.1 redo log:保证“已提交不丢”
redo log是InnoDB存储引擎层的日志,记录的是物理修改:“某个表空间的第某个页、第哪个偏移量、被改成了什么”。它是循环写的,文件名通常是ib_logfile0、ib_logfile1等,全部写完后会覆盖从头再写。因为循环写,所以redo log不可能无限保留历史,这也是为什么它只负责崩溃恢复,不负责归档备份。
写入redo log时,InnoDB先把记录写到log buffer(内存),在事务提交时根据innodb_flush_log_at_trx_commit的值决定何时刷盘:
| 参数值 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| 1 | 每次事务提交都刷盘 | 不丢已提交事务 | 慢,但可靠 |
| 0 | 每秒刷一次,提交时不刷 | 最多丢1秒日志 | 快,但不安全 |
| 2 | 每次提交写入OS缓存,每秒刷盘 | OS崩溃可能丢1秒 | 折中 |
生产环境我建议必须设为1,这是“已提交事务不丢”的最低保证。如果为了追求性能调成0或2,那么数据库或服务器掉电时,最近1秒内提交的事务可能从redo log里消失,业务数据一旦丢了就找不回来。作者曾经见过一个支付对账系统的库设置成0,结果物理机断电后丢了几个订单,后来费了很大劲才手工补上。这个教训很值钱。
另外,innodb_log_file_size决定了redo log文件的整体大小。太小会导致InnoDB频繁触发检查点来推进覆写,增加刷盘频率;太大又会让崩溃恢复变慢。MySQL 8.0默认已经把innodb_log_file_size提到48M,但生产上我会根据写入量调整,比如单机每天写日志几十GB时,设置成1G~2G是比较平衡的选择。
4.2 undo log:回滚与MVCC的基石
undo log和redo log刚好相反,redo是“重做”,undo是“回滚”。每条修改语句执行前,InnoDB会生成一条undo log,记录行的旧值。事务回滚时,依靠undo log把行改回原来的样子。注意,它记录的是逻辑变化,并不是物理页镜像,也就是说“把这行的balance减去100”这种逻辑操作,而非“把字节xx改成yy”,这样多个版本链才能串联起来。
undo log最大的作用是支撑MVCC(多版本并发控制)。InnoDB的行记录里有一个隐藏字段DB_TRX_ID,表示最近修改该行的事务ID,而undo log里能找到更早版本的记录。当普通SELECT(非锁定读)发生时,InnoDB会根据当前事务的快照,顺着undo版本链找到符合隔离级别要求的旧版本。RR隔离级别下,第一次SELECT建立read view,之后都基于这个视图读取;RC级别下,每次SELECT都重新建立视图。这就是为什么RR能解决不可重复读,而RC不能。
事务提交后,undo log并不是立即删除。因为可能还有更早启动的读事务需要访问旧版本,InnoDB要靠purge线程在确认没有事务会再引用这些旧版本后,才逐步清理。很多人看到information_schema.INNODB_TRX里的history list length飙升,就说明undo purge跟不上,常见原因是存在长时间未提交的读事务,导致undo无法清理。碰到这个情况,要先找出跑了几小时的事务,而不是一味调参数。
4.3 binlog:主从复制与时间点恢复
binlog属于MySQL Server层,和引擎无关,所有引擎都能用。它记录的是逻辑操作,可以是SQL语句,也可以是行变更(ROW格式)。它追加写,内容会一直保留(只要磁盘够大),相比redo log的循环写,binlog更像是“完整账本”,用来做主从复制和数据恢复。
主从复制时,主库把自己的binlog发给从库,从库把binlog写入自己的relay log,然后按顺序执行,从而保持数据一致。如果你的从库是级联结构,从库还会生成自己的binlog继续往下传。在这个链路里,binlog的格式直接影响数据一致性和同步成本:
STATEMENT格式:记录SQL本身,优点是日志量小,但NOW()、UUID()这类函数在主从执行的结果可能不同,容易造成数据不一致。ROW格式:记录每行数据的变更前/后值,最安全,缺点是日志量大。MIXED:默认自动切换,MySQL自己判断哪些语句用ROW。
我的建议是直接使用ROW。虽然日志量大了不少,但能在误操作后精确恢复单行数据,排查问题时也能直接从binlog事件里看到旧值和新值,这一点对审计和容灾非常重要。关于binlog_row_image,默认FULL会记录整行前后镜像;如果业务允许,可以设成MINIMAL,只记录被修改的列和需要回表的键列,能显著减小日志体积,但要注意某些基于binlog的解析工具可能会受影响。
4.4 三种日志的协作关系(两阶段提交)
事务提交时涉及三个日志的写顺序,这是MySQL数据一致性的核心。redo log在事务执行过程中就会不断写入(记录在内存,提交时刷盘),undo log也是随着每条写操作一起生成,而binlog是在事务提交阶段才由Server层写入。如果binlog写入成功但redo log没有刷盘,崩溃后从库拿到binlog,主库丢了这个事务,主从数据就不一致了。所以InnoDB引入了“两阶段提交”。
两阶段提交的简化过程:
- InnoDB先为事务生成prepare状态的redo log,并把事务标记为prepare。
- Server层写binlog,并调用引擎提交事务。
- InnoDB收到提交指令后,把redo log状态变为commit。
崩溃恢复时,MySQL会扫描最后一个binlog里记录的XID,比对redo log里的prepare事务列表。对于“redo prepare了但binlog没写”的事务,判定为回滚;对于“redo prepare了且binlog已写成功”的事务,即使redo没有commit标记,也判定为提交并重做。这样就保证了两边日志永远一致。这也是为什么调整sync_binlog和innodb_flush_log_at_trx_commit时,必须考虑两者配合:最稳妥的是都设为1,让binlog每次提交刷盘,redo也每次提交刷盘。如果为了让性能好一些,把sync_binlog设成0而redo设成1,可能牺牲主从一致,主库在崩溃恢复时可能判定某些事务已提交,但binlog并没有落盘,从库就会少数据。
5. 崩溃恢复与关键配置参数
5.1 崩溃恢复流程复盘
假设MySQL在写入大量事务时突然宕机,重启后会发生什么?InnoDB会扫描redo log里Checkpoint之后的日志,利用LSN分析出哪些页需要重放。它会先把日志里的相关数据页加载到缓冲池,然后从最早的LSN开始按顺序应用日志,直到把日志耗尽。此时,所有已提交但还没刷盘的事务都会被重放至数据页;而处于prepare状态但binlog未记录的事务,则根据两阶段提交规则回滚掉。
恢复时间取决于两个因素:Checkpoint落后多少、redo log有多大。如果想要快速恢复,必须维持合理的检查点频率,而检查点由系统自动调度,我们能做的是调大innodb_log_file_size来减少日志切换引发的检查点,但不能无限调大,否则恢复时要扫描的日志段更多。日常运维中,可以在SHOW ENGINE INNODB STATUS里查看LOG部分,里面有Log sequence number和Last checkpoint at两个值,二者差值过大时就需要关注刷脏能力。
一个典型的倒置问题是:很多人为了“绝对安全”把日志文件调得巨大,结果MySQL每次崩溃恢复要跑10分钟以上,业务等不起。这就要平衡:如果服务等级允许秒级恢复,日志文件不能超过某个阈值;如果允许分钟级,就可以大方一点。我的经验是,在普通SSD上,单实例redo log设置512M~2G是相对稳妥的范围,具体还要根据TPS和磁盘吞吐做压测。
5.2 双写缓冲与页损坏处理
把脏页刷盘时,最怕遇到“partial page write”:一个16KB的页写到一半,系统断电,这个页就既不是旧版本也不是新版本,而是残缺页。由于redo log记录的是物理页内的修改,如果基础页本身就是坏的,重放时也救不回来。InnoDB的解决方案是双写缓冲(doublewrite buffer)。
双写机制是:先把脏页复制到内存中的doublewrite buffer,然后一次性顺序写入表空间里的一块连续区域(doublewrite文件),最后再把页写到实际位置。这样即使实际位置的页写坏了,还能从doublewrite文件中找到完整备份,再配合redo log完成恢复。在MySQL 8.0里,参数innodb_doublewrite默认开启,如果磁盘是新式NVMe SSD且电池保护做得好,有些DBA为了追求性能会关掉它,但我个人不推荐在生产库上关闭,这个机制的成本大约5%~10%的写性能,换来的却是“页损坏”这个老大难问题的兜底。
万一真的发生页损坏,比如报错InnoDB: Database page corruption on disk or a failed memory-mapped write...,不要慌。先停掉MySQL,用表空间ibd文件做物理备份,再尝试用innodb_force_recovery参数启动,从1到6逐步加,但要注意force_recovery大于0时数据库只读且禁止修改,目的只是导出数据。数据导出后重建表,才是正规处理方法。平时一定要做好mysqldump逻辑备份,因为它能在物理文件损坏时保住数据。
5.3 日志相关重要参数推荐与优化
实际项目里,我建议把下面这一组参数作为初始化模板,再根据机器规格和业务形态微调:
[mysqld] innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 binlog_format = ROW binlog_row_image = MINIMAL innodb_log_file_size = 1G innodb_log_files_in_group = 4 max_binlog_size = 1G expire_logs_days = 15说明一下:innodb_log_files_in_group乘以innodb_log_file_size是redo日志组总量。MySQL 8.0.30以后新增了innodb_redo_log_capacity参数来动态配置redo容量,并替换掉了旧的文件数量参数,如果你用的版本较新,直接设innodb_redo_log_capacity=4G更省心。expire_logs_days在MySQL 8.0已经废弃,改用binlog_expire_logs_seconds,想保留15天就设置成1296000。
还有一个容易被忽略的参数:innodb_flush_method。Linux下推荐设置为O_DIRECT,让InnoDB的数据文件读写绕过操作系统页缓存,避免双重缓存导致内存浪费和刷盘不确定。如果设置成fsync,数据会先到OS cache再刷到磁盘,虽然小写入可能更快,但宕机时数据在OS cache中没落盘,又增加一层丢失风险。O_DIRECT不是绝对最快,但它让数据库的行为更可预期,适合有独立存储的环境。
6. 实践经验:磁盘、参数调优与问题排查
6.1 日志文件大小怎么定
很多人在测试环境随手设了一个小日志就上生产,结果业务高峰期频繁检查点,IO打满,查询延迟飙升。怎么确定合理大小?我的办法很简单:观察运行期间redo日志的写入速度。在业务压力稳定时,用SHOW ENGINE INNODB STATUS里的Log sequence number每隔10分钟采样两次,算出每分钟写入的MB数,然后保证日志组总容量至少能容纳10分钟以上、最好30分钟的redo量。比如每分钟写入200MB,那日志组总量最好达到6G以上,否则InnoDB会频繁推进检查点来腾出空间,刷脏成为持续瓶颈。
binlog大小同样要规划。不能等磁盘满了才去清理,也别让binlog保留太多天。我一般按每天binlog写入量估算磁盘占用,预留至少1.5倍空间,并用crontab或mysqlbinlog定期备份到异地。记住:binlog是误删恢复的救命稻草,“保留时间”和“磁盘成本”之间需要明确取舍。
6.2 误删与恢复实操(基于binlog)
这个场景我在工作中处理过不止一次:业务同学半夜连删了好几张表的数据,第二天才发线。操作步骤大家可以收藏备用:
- 先确认binlog是否开启,找到最近的全量备份时间点之后的binlog文件。
- 用
mysqlbinlog把目标时间段的binlog解析成SQL文件,最好加上--base64-output=DECODE-ROWS,以可读的ROW格式查看。 - 定位误操作前的最后一个位置(通过时间戳或者具体event的position),把误删除的语句块过滤掉。
- 只恢复这部分增量。如果数据量不大,可以用
mysql客户端直接导入;如果量大,建议导入到临时实例,验证后再合并回主库。
举个例子,如果今天00:00误删了一张表,昨晚23:30有全量备份,那么先恢复全量备份到临时实例,再用binlog把23:30到00:00之间的增量应用上去,并且要把误删的DELETE过滤掉。mysqlbinlog的--stop-position和--start-position要精确到event。为了提升恢复效率,尽量在ROW格式下把事务分组,避免大事务占用超长锁。恢复完成后,第一时间对比表和行的校验值,确认数据一致再切流量。
这个恢复流程一定要在测试环境先演练一次。不要等真出事了再临时查文档,那时候心理压力大,很容易手抖执行错位置。
6.3 排查慢刷、锁等待与日志暴涨的步骤
遇到“查询突然变慢”,不要一上来就杀慢SQL,先看是不是刷脏引起的。检查顺序:
1. SHOW ENGINE INNODB STATUS \G -> 看BUFFER POOL AND MEMORY里的Modified db pages占比 -> 看LOG里的Log sequence number和Last checkpoint at差距 2. iostat -x 1 -> 看磁盘%util和await是否过高 3. 查information_schema.INNODB_TRX -> 看看有没有长事务如果Modified db pages一直高位运行,说明脏页堆积严重,可能是innodb_max_dirty_pages_pct设置过大,或innodb_io_capacity设置低于实际磁盘能力。把后者调到足够值,并适当降低脏页比例阈值,让刷脏更积极。如果历史链长度(History list length)暴涨,重点查有没有未提交的长事务,因为事务不提交,undo一直清不掉,版本链越长,查询回放旧版本的成本越高。
另一个常见坑是“binlog暴涨”。排查时先看SHOW MASTER STATUS确认当前binlog文件大小,再使用mysqlbinlog查看里面有多少条大事务。RMW格式下,一个大的UPDATE/DELETE会生成超大的binlog事件,甚至单个事务好几个GB。这类大事务会拖垮主从复制延迟,解决办法是在业务侧拆分事务,比如按主键分批更新,每次影响几千行而不是几十万行。具体分批SQL可以用WHERE id < ?和循环实现,配合每批sleep一小段时间,能有效缓解日志风暴和锁竞争。
还有一个经验:不要把所有日志都放在系统盘或根目录。把datadir和binlog目录分别挂载到不同的数据盘上,可以避免日志I/O和数据文件I/O互相争抢,也能防止根目录写满导致整个机器hang死。磁盘空间监控的阈值建议设置为85%报警,90%就要立刻处理,binlog及时清理,data目录保持足够余量。
最后再分享一个我在实际维护里的习惯:每周挑一个低峰期,做一次“日志恢复演练”,从全量备份开始,应用binlog到某个最近时间点,比对关键业务表的行数。这个过程虽然费点时间,但能提前暴露备份脚本、binlog参数、恢复流程里的所有问题。数据库稳定运行靠的不是运气,而是把每条日志机制都吃透,并且验证过它能真正做到什么。InnoDB底层原理和MySQL日志机制讲得再多,都不如自己动手把日志翻一遍、把恢复流程跑一遍来得实在。