ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL InnoDB核心机制:从SQL执行到事务日志的完整解析

MySQL InnoDB核心机制:从SQL执行到事务日志的完整解析 1. 一条SQL从客户端到InnoDB的完整路径1.1 连接器会话不是一锤子买卖你打开终端敲下mysql -u root -p输入密码回车看到欢迎横幅的同时MySQL服务端实际上只做了一件看起来简单的事创建一条会话。这一步不是InnoDB干的而是MySQL Server层的连接器负责的。连接器会做TCP握手、校验用户名和密码、读取当前账号的权限元数据然后把这个会话挂到线程池或临时线程上。从这一刻开始你在这个会话里能操作哪些表、哪些字段不再每次请求时都查一遍MySQL库而是直接使用连接器在握手时缓存下来的权限集。这也是为什么线上给账号改了权限后要求业务重新建立连接才生效——不是系统反应慢是权限集在会话建立时就已经固定了。连接器还负责处理那些“看起来像SQL问题”的坑。比如报错MySQL server has gone away经常是wait_timeout或interactive_timeout把空闲连接断掉了业务侧连接池里的旧连接还没感知。又比如max_allowed_packet设置太小一个批量插入的报文发不过来连接器会直接拒绝。再比如认证插件不匹配客户端是旧版libmysqlclient服务端是MySQL 8.0默认的caching_sha2_password也会卡在握手阶段。这里我建议你排查Session超时问题时先看performance_schema里的events_statements_current而不是一上来就追SQL执行计划。会话层的问题特征很明确SQL根本没进去慢日志里没有记录。1.2 解析与优化语法树和成本模型连接器验明正身之后SQL进入解析器。解析器做的是词法分析和语法分析把select * from t where id 1拆成关键字、表名、字段、常量然后构建一棵语法树。这一步只校验“你能不能写出这句话”不校验“这张表存不存在、这个字段对不对”。所以一旦报Unknown column说明已经过了语法检查是在语义阶段才失败的你直接怀疑字段名拼写就行。语法树构建完真正的决策者是优化器。MySQL优化器不是“硬猜”的它基于存储引擎提供的统计信息计算各种执行路径的成本。比如全表扫描要读多少页、走二级索引要回表多少次、多表连接先连哪张更划算最终选一个它认为成本最低的执行计划。很多人以为WHERE条件的书写顺序会影响执行计划其实5.7和8.0的优化器不会这么傻它会把条件重排列但如果你用了OR连接多个非索引条件优化器往往只能选全表扫描这不是优化器笨而是没有更好的路径可选。这里有一个容易被忽略的点优化器选错索引往往不是优化器的问题而是统计信息不够新。如果一张表频繁增删改但ANALYZE TABLE从没跑过优化器拿到的“大概有多少行”可能严重失真。我在线上遇到过一张表实际只有2万行但统计信息告诉优化器有200万行它死活不走索引FORCE INDEX之后执行时间从2秒降到20毫秒。所以看到不合理执行计划先别骂优化器先检查information_schema.statistics和表的行数估算。1.3 执行器与存储引擎接口谁在真正干活执行计划确定后执行器登场。执行器首先会判断当前用户是否对目标表有权限这也是为什么你在连接器阶段权限校验没错但执行具体SQL时仍可能报权限不足。然后执行器调用存储引擎的Handler接口把计划一步步落下去。对InnoDB来说执行器说“我要从第1行开始扫描”InnoDB就从缓冲池或磁盘里取回第一个满足条件的行执行器说“这行不满足WHERE”InnoDB就继续取下一行直到InnoDB返回“没有更多行”。这个分工很关键Server层只负责“编排”真正读取数据页、判断索引范围、返回行数据的是InnoDB。执行器每次从InnoDB拿一行都会累加rows_examined慢日志里的Rows_examined字段就是执行器“向存储引擎要了多少行”而不是“最终返回了多少行”。理解了这条路径你再看最典型的慢查询一张表建了索引但没走执行器大概要扫描几十万行走了索引但回表次数过多执行器向InnoDB发起的单页读取也会高达几万次。这两类问题的本质都不一样前者是优化器选路问题后者是数据组织方式问题得用不同手段去解。2. InnoDB的内存与磁盘布局数据到底放在哪2.1 页、区、段和表空间MySQL 8.0里一张InnoDB表的数据和索引最终落在表空间文件中。逻辑上InnoDB把空间划分成段Segment、区Extent、页Page三层。一页默认16KB一区默认1MB包含连续64个页段是B树索引的管理单位一个索引至少对应两个段叶子节点段和非叶子节点段。很多人天天说B树但没意识到B树不是纯逻辑概念它的每一个节点都对应一个16KB的物理页。页是InnoDB和磁盘交互的最小单位哪怕你只查询一行记录InnoDB也要把整页16KB读进内存。为什么要扯这个结构因为在理解IO放大时这是底层依据。你在二级索引上等值查到一条记录回表读取聚簇索引时InnoDB读取的不是“那条记录”而是包含那条记录的整个页。一次回表往往要一次随机读一次随机读在传统机械硬盘上约消耗几毫秒如果SQL需要回表几万次慢是必然的。后来SSD普及了随机读延迟降到几十微秒但也不能无脑回表覆盖索引的思路依然很重要。B树能存多少行也可以从页大小直接估算。假设主键是BIGINT8字节页指针6字节非叶子节点一条索引项占14字节一个16KB页大约能放1170个索引项。假设一行数据1KB一个叶子页放16行。三层的B树第一层是根第二层1170个分支第三层叶子页数量就是1170×1170约137万个页乘以16行约2190万行。这就是为什么InnoDB索引深度通常3到4层就能支撑千万级表。如果主键从8字节变成128字节的UUID非叶子节点一条索引项占134字节一个页只能放约122个索引项同样三层树最多只能支撑约24万行性能断崖式下降。顺便说一句这就是我一直反对无脑用UUID当主键的原因它不只是随机写问题也在数学上减少了一棵树的容量。2.2 Buffer Pool与Change Buffer内存命中率决定快慢理解了页之后再看InnoDB的缓存机制就顺了。所有数据页的读写都先经过Buffer Pool。理想状态下一个热点表的所有页都常驻内存SQL基本不产生磁盘IO如果内存放不下InnoDB只能按照LRU策略淘汰冷页。但InnoDB对LRU做过分代默认前5/8是young区后3/8是old区新读入的页先进old区只有再次被访问才晋升到young区。这个设计是为了防止全表扫描一次性把热数据全部冲掉。你可以通过Innodb_buffer_pool_read_requests和Innodb_buffer_pool_reads两个状态值算命中率命中率长期低于99%的库优先加大Buffer Pool而不是去调一堆看似高深的参数。Buffer Pool里还藏着一个容易被低估的结构Change Buffer。以前它叫Insert Buffer后来扩展成对二级索引的UPDATE、DELETE也生效。二级索引的写入往往是随机的如果每次插入都直接去磁盘改二级索引页代价很高。ChangeBuffer的做法是先把对二级索引的修改缓存在内存里等这个索引页因为其他查询被读入Buffer Pool时再把这批修改合并进去。这个设计让“到处乱插”的二级索引写入变成顺序缓冲但代价是崩溃恢复和刷脏逻辑变得更复杂。MySQL 8.0里Change Buffer默认占用Buffer Pool的25%如果业务写多读少这个比例可以适当调高如果是读写均衡型保持默认就够了。2.3 聚簇索引与二级索引为什么主键不能随便选InnoDB的表是索引组织表数据行存储在聚簇索引的叶子节点里。聚簇索引通常就是主键索引如果你没有定义主键InnoDB会找一个没有NULL值的唯一键作为聚簇索引再找不到就生成一个隐藏的ROWID。所以“我没有建主键”不代表表没有主键只是你在被动接受一个不可见的、完全随机的ROWID这种表做范围查询和备份恢复都不舒服。二级索引的叶子节点存储的是主键值而不是行的物理地址。这个设计的巧妙之处在于主键改变时二级索引不需要同步更新但糟糕之处在于每次通过二级索引查数据除了扫描二级索引页还要根据主键值去聚簇索引里重新定位一次这就是“回表”。如果二级索引已经包含了所有需要返回的字段执行计划会直接用“覆盖索引”跳过回表Extra列显示Using index。这是索引优化里最实用的手段把高频查询中WHERE和SELECT涉及的关键列都放进同一个联合索引一次索引扫描就把数据拿完。回到主键选择自增主键之所以好不只是因为它简单。自增ID顺序插入新记录大概率落在当前最右侧的叶子页页分裂少空间利用率高。而UUID主键每次插入的位置都是随机的B树中间的页不断发生分裂和重组不仅写入变慢还会留下大量碎片页。如果你一定要用业务主键至少应该选趋势递增的雪花ID而不是毫无顺序可言的随机字符串。3. 事务隔离与锁InnoDB如何不让自己乱套3.1 隔离级别不是配置出来的是ReadView和锁一起撑起来的MySQL的事务隔离级别是面试高频但如果只背“RC会产生幻读、RR不会”遇到实际问题照样抓瞎。在InnoDB里隔离级别是ReadView读视图机制和锁机制组合出来的结果。普通SELECT走的是MVCC快照读不加锁SELECT ... FOR UPDATE、UPDATE、DELETE走的是当前读必须加锁。ReadView的作用是给事务定义一个“可见版本边界”。事务执行过程中每行数据可能被多个事务改过InnoDB通过undo log把这些版本串成一条版本链。ReadView里记录了当前活跃事务的最小ID和最大ID判断某个版本是否可见就用版本的事务ID和ReadView的边界去比。RC和RR的核心区别在于RC下每个普通SELECT语句都会生成一个新的ReadView所以一个事务里两次查询能看到其他事务新提交的数据于是可能出现不可重复读RR下只在事务第一次执行SELECT时生成ReadView之后整个事务都沿用这个快照因此“快照读”天然不会看到其他事务的更新。真正让RR和RC拉开差距的是“当前读”。RR下InnoDB除了锁住目标记录还会锁住目标记录周围的间隙防止其他事务往这个范围里插入新行。这就是为什么RR能一定程度防幻读而RC不太防。注意这里说的是“一定程度”因为RR的MVCC只保证快照读不出现幻读如果你在同一个RR事务里先跑了一次SELECT再执行SELECT ... FOR UPDATE做当前读仍然可能看到新插入的行。所以“RR完全避免幻读”这个说法不严谨。3.2 锁的类型与加锁规则InnoDB的锁可以从粒度分成表锁和行锁。表锁最常见的是MDL元数据锁它保护表结构在DDL期间不被修改。行锁则细分为三类Record Lock记录锁只锁索引记录本身Gap Lock间隙锁锁住记录之间的开区间阻止其他事务在这个区间插入Next-Key Lock临键锁是记录锁和间隙锁的组合锁住的是“当前记录以及它前面的间隙”。在RR隔离级别下InnoDB的默认加锁单位就是Next-Key Lock这也是它与RC最显著的区别。很多人搞不清楚“唯一索引等值查询为什么有时没有间隙锁”。规则其实清晰当等值查询命中的是唯一索引时优化器知道目标记录唯一只需要加一个Record Lock不需要锁间隙。但如果这个等值查询没有命中任何记录比如查id9而表里id分别是1和10InnoDB会认为“防止在9这个位置插入新行”是必要的于是加一个间隙锁。范围查询则更复杂WHERE id BETWEEN 8 AND 10 FOR UPDATE不仅会锁住8到10之间的已有记录还会锁住它们之间的间隙以及边界让其他事务无法插入新记录。理解了这套规则你基本就能解释为什么RR隔离级别高并发下死锁概率更高大家互相锁住的间隙更多冲突面自然变大。还有一个很常见的坑如果WHERE条件没有命中任何索引InnoDB只能全表扫描所有聚簇索引记录每个记录都会加锁相当于把整张表锁住了。这不是表锁但效果比表锁更隐蔽因为SHOW PROCESSLIST里看不到LOCK提示只有阻塞链和锁等待超时能暴露问题。排查时优先确认SQL的WHERE列是否有可用索引尤其是高频UPDATE和DELETE语句。3.3 死锁是怎么来的又该怎么解死锁的本质是加锁顺序不一致形成资源的循环等待。比如事务A先锁id1再想锁id2事务B先锁id2再想锁id1两个事务同时推进就必然有一个事务阻塞最终InnoDB的死锁检测会回滚其中一个。你会在客户端看到Deadlock found when trying to get lock; try restarting transaction错误错误码通常是1213。InnoDB选择回滚“代价较小”的事务这个代价是估算修改行数和锁数量得出来的。解死锁的思路第一步永远是先看清锁等待关系。你可以执行SHOW ENGINE INNODB STATUS看LATEST DETECTED DEADLOCK段里面会打印最近一次死锁涉及的SQL语句、持有锁的KEY值、事务开始时间和回滚选择。通过日志你能准确还原加锁顺序。第二步是修改业务代码尽量让所有事务按同一顺序访问资源。比如多个事务都要操作账户1和账户2就统一先锁ID小的账户。第三步是缩小事务范围把无关查询挪到事务外减少事务持锁时间能用一条UPDATE解决就别拆成两条能在RC隔离级别下满足业务就尽量别用RR减少间隙锁带来的死锁概率。4. 三种日志的分工与协作redo log、undo log、binlog4.1 redo log物理日志为什么必须小InnoDB最核心的持久性保障来自redo log。InnoDB修改数据页时并不会立刻把脏页刷到磁盘而是先写redo log。这就是WALWrite-Ahead Logging写日志先行。为什么能这么做因为刷数据页是随机IO而写redo log是连续追加IO顺序写比随机写快几个数量级。把随机刷盘变成顺序写日志再把真正刷脏页交给后台异步线程整体性能就能拉高一大截。redo log本质上记录的是“对某个物理页的某个偏移量做了什么修改”属于物理日志。它是循环写的文件组里的多个日志文件写满一圈后会被覆盖。数据库在后台推进Checkpoint把此前已经刷到磁盘的脏页对应的日志空间标记为可重用。如果Checkpoint推进太慢redo log写满InnoDB会强制执行脏页刷新这时候系统会出现“写盘抖动”。线上如果观察到周期性IO飙升很多情况下是innodb_log_file_size设置太小。5.7时代默认只有48MB对写密集型业务来说太小我一般建议单实例在512MB到2GB之间具体还要看峰值写入量和刷脏速度。注意太大也有代价崩溃恢复时需要重放的日志范围更大启动恢复时间会变长。innodb_flush_log_at_trx_commit参数直接决定redo log在事务提交时的刷盘策略。这个参数有三个值我建议用下面的表格来对照理解参数值提交时行为可能丢失范围适用场景0不主动刷盘由后台线程每秒刷一次MySQL崩溃时最多丢失最近1秒的已提交事务日志型、流水型数据能容忍少量丢失1每次提交都强制fsync刷盘不丢失账户、订单、支付等强一致场景2写入操作系统缓存由OS每秒刷盘操作系统崩溃或断电时最多丢1秒MySQL进程崩溃不丢大多数在线业务允许极端情况丢1秒一致性要求越高的系统越应该用1。如果用了2最好同时让binlog也保证落盘否则主从复制可能追不上。4.2 undo log回滚和MVCC都靠它undo log是逻辑日志和redo log不同它记录的是“如何撤销这次操作”。事务执行一条UPDATEundo log里会记一条“原来这行的旧值是xxx”事务执行一条DELETEundo log里会记一条“被删掉的行长什么样”。一旦事务要回滚InnoDB就去undo log里找对应的反向操作把数据恢复成原样。这是原子性的底层保障。但undo log不只是给回滚用的它还支撑MVCC版本链。前面提到的ReadView判断某个行版本是否可见靠的就是沿着undo log从最新版本往前遍历找到第一个满足可见性条件的版本。这带来一个非常容易被忽略的问题一个长事务虽然没有写入但只要它一直持有ReadView那些“旧版本”就不能被清理undo表空间会持续膨胀。我遇到过测试环境一个连接把事务开着半天不提交binlog没涨undo却涨到几十GB。清理这些undo log麻烦得很根本不像删除binlog那么直接。所以线上严格监控长事务不仅是业务上的好习惯更是InnoDB日志机制的客观要求。4.3 binlogServer层的逻辑日志redo log和undo log都是InnoDB存储引擎层的东西binlog则是MySQL Server层的日志。它记录的是逻辑变化要么是原始SQLSTATEMENT格式要么是每一行数据的前后镜像ROW格式。默认场景下binlog有两个核心用途主从复制和时间点恢复。主库把binlog发给从库从库重放这些事件数据就同步过去了数据库误删了数据也可以用全量备份binlog把数据滚到故障前的一秒。binlog的格式选择直接影响主从一致性。STATEMENT格式日志体积小但同样的SQL在从库执行结果可能因为随机函数、存储过程等原因不一致。ROW格式记录逐行变化日志体积大但最安全主从差异基本不会出现。MySQL 8.0默认就是ROW格式这在实际运维中省了非常多事。另外sync_binlog参数控制binlog的刷盘策略配合单个事务里的binlog事件尽量在binlog写入时也做到落盘。对强一致场景一般建议sync_binlog1让binlog和redo log在“提交不丢”这件事上对齐。4.4 两阶段提交与崩溃恢复如果你只记住一个关于MySQL日志机制的知识点我觉得应该是两阶段提交。为什么需要它因为redo log在InnoDB手里binlog在Server层手里两个组件各自维护各自的落盘状态。如果事务先提交到InnoDB但binlog还没写就崩溃了重启后主库数据是新的从库却没有这条变更如果先写binlog但redo log没提交主库数据回滚从库却已经应用了这条日志。两条路都会主从不一致。InnoDB的内部XA事务解决了这个问题过程大致是这样事务执行完修改后InnoDB进入prepare状态把redo log刷盘取决于innodb_flush_log_at_trx_commitServer层随后写入binlog并落盘binlog写入成功后InnoDB再把事务标记为commit。这套流程的关键点是崩溃恢复时如果redo log事务处于prepare状态InnoDB会让Server层去查binlog里是否有对应的XID事件有就提交没有就回滚。也就是说binlog是否写入成功成了“要不要承认这个事务”的判定依据。这个机制也让崩溃恢复过程非常清晰。数据库启动时InnoDB利用最后Checkpoint的位置从redo log中找出所有没有刷盘或没有提交的事务逐一重放或回滚。所以你会看到MySQL重启后自己打印恢复日志等待一段时间才能接受连接。数据量越大、redo log越大、Checkpoint越久、恢复耗时越长。运维上周期性推进Checkpoint、及时清理长事务比在崩溃后祈祷恢复更快靠谱得多。5. 底层机制如何影响日常SQL性能从参数到执行计划5.1 参数不是越大越好很多刚接触MySQL调优的人喜欢照着网上的“万年配置模板”一顿改最常见的是把innodb_buffer_pool_size调得特别大、把innodb_log_file_size改成2GB、把innodb_flush_log_at_trx_commit设为0。这些参数没有绝对的最佳值只有和业务匹配的值。先说Buffer Pool。它用来缓存数据页但数据库总内存里还有排序缓冲、连接线程、各种内部结构不能全塞给它。我的经验是初始设为物理内存的50%观察命中率后再微调到60%-70%。如果内存只有8GB却把Buffer Pool设置成6GB内存不足会导致操作系统换页整个数据库响应曲线直接变成锯齿状。你还得记住Buffer Pool再大也挡不住没有索引的全表扫描因为扫描过程中新读入的页会不断把热页顶出缓存这个场景下LRU分代只是缓解不是根治。然后是innodb_flush_log_at_trx_commit和sync_binlog这对组合。在“双1”配置下每次事务提交要等两次落盘性能最差但最安全。现实中我见过一个支付核心库被迫从双1改成2加sync_binlog0结果一次物理机重启丢了最近一秒钟的交易流水业务方差点炸毛。后来他们老老实实用回双1用批量提交和合并写来提升吞吐。记住一个原则性能瓶颈永远优先用索引、减少扫描行数来解决而不是用牺牲持久性去换TPS。5.2 为什么DELETE后表文件没变小这是一个经常被问到的“空间之谜”。你执行DELETE FROM t WHERE create_time ...删掉了上百万行再看ls -lh表文件大小几乎没变化。原因还是要回到InnoDB的页组织上DELETE只是把记录在B树里的位置标记为已删除把它挂到了一个可复用的链表上并不会自动把页里的空间归还给文件系统。后续如果插入一条尺寸相近的记录InnoDB可以优先复用这些“空洞”所以大表删除后继续写入文件大小不会继续快速增长。但如果你删完之后再也不写入这张表的空间就白白占着。真正的释放需要重建表。常用的手段是OPTIMIZE TABLE或ALTER TABLE t ENGINEInnoDB它们的本质都是新建一张表把数据按B树重新组织最后切换回去。注意这个操作会消耗临时磁盘空间如果在空间不足的实例上执行反而会把磁盘填满。在8.0里OPTIMIZE TABLE多数情况下是Online操作但建议仍在低峰执行。更复杂的表结构变更可以用pt-online-schema-change或gh-ost这类工具它们把重建过程拆成小批量对线上影响更小。和DELETE形成鲜明对比的是TRUNCATE它直接DROP段再重建所以表空间会立即缩小。5.3 排序和索引失效最常见的一类慢SQLMySQL慢查询里Using filesort和Using temporary出现频率极高。它们的根源都一样优化器找不到一条能让数据“本来就是目标顺序”的索引路径只能把数据搬到临时空间排序。InnoDB的B树叶子节点本来按索引键顺序排列所以如果ORDER BY字段是某个索引的一部分且前面的等值条件让这个字段在索引里保持有序优化器就能直接按索引扫描输出连排序都省了。反之如果排序字段在联合索引的第N列而前N-1列的条件是范围查询或缺失排序字段的顺序就被打断只能额外排序。举个具体例子。订单表有联合索引(user_id, status, create_time)SQL是SELECT order_id, amount FROM orders WHERE user_id 123 AND status IN (0, 1) ORDER BY create_time DESC LIMIT 20;执行计划里很可能出现Using filesort。原因在于status IN (0, 1)是范围条件当进入第二个值范围时create_time的有序性已经被打破B树无法保证整体按create_time排列。遇到这种查询更合适的索引是(user_id, create_time)让user_id确定等值后create_time天然有序排序直接由索引解决。少一次临时排序大数据量下可能差几十倍。索引失效的另一大类是隐式转换和函数操作。WHERE phone 13800000000如果phone是VARCHARMySQL会把字符串列转成数字再比较等于在列上做了函数操作索引自然用不上WHERE DATE(create_time) 2025-01-01也是同理。排查这类问题时别只看执行计划里有没有走索引还要看type是不是ref或range如果出现index甚至ALL基本就是索引条件被破坏了。我会在SQL前端加一层规范检查禁止在WHERE条件里对索引列做任何运算这是成本最低的预防方案。6. 把原理串起来一次真实死锁和一次崩溃恢复的复盘6.1 死锁复现两个账户两条相反更新路径前面讲了死锁理论这里我放一个实际复现过的场景。表结构很简单CREATE TABLE account ( id INT PRIMARY KEY, balance DECIMAL(10,2) NOT NULL ) ENGINEInnoDB;事务A执行START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 1; -- 停顿片刻保证A持有id1锁 UPDATE account SET balance balance 100 WHERE id 2; COMMIT;事务B执行START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 2; -- 停顿片刻保证B持有id2锁 UPDATE account SET balance balance 100 WHERE id 1; COMMIT;如果两个事务并发执行到“停顿”之后A持有id1、想拿id2B持有id2、想拿id1两个事务立刻形成循环等待。InnoDB的锁等待超时参数innodb_lock_wait_timeout默认50秒但死锁检测机制会在更短时间内发现环直接回滚一个事务。客户端会收到类似“Deadlock found when trying to get lock; try restarting transaction”的异常。打开SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK段能看到两个事务的SQL和加锁信息。这类死锁在账户转账场景里太典型了。我的修复思路是所有涉及多账户更新的操作先对账户ID排序统一按从小到大加锁。A和B都先锁id1再锁id2就不会出现互相争抢的环。更极致的方式是用一条SQL完成双向余额变更比如UPDATE account SET balance balance - IF(id 1, 100, -100) WHERE id IN (1, 2);这样InnoDB会按主键顺序加锁天然避免交叉。就算业务无法改代码至少也应用SELECT id FROM account WHERE id IN (1,2) ORDER BY id FOR UPDATE手动统一加锁顺序。6.2 崩溃恢复复盘kill -9后数据还能回来吗日志机制到底靠不靠谱我最喜欢用测试环境直接模拟一次崩溃。操作流程如下先把innodb_flush_log_at_trx_commit1、sync_binlog1设置好重启MySQL生效连接后开启事务插入一行数据并COMMIT在提交成功后的几秒内直接kill -9MySQL进程然后重新启动MySQL服务观察启动日志。启动时大概率能看到InnoDB在做崩溃恢复相关的检查MySQL会先扫描最后一次Checkpoint之后的redo log把可能处于prepare状态的事务和binlog对照。因为我刚才COMMIT的数据已经完整写入了redo logbinlog也落盘了所以重启后查这张表新插入的行应该在。这就是WAL和两阶段提交共同保证的结果数据不会因为进程被杀而消失。相反如果把innodb_flush_log_at_trx_commit0在事务提交后立刻杀进程重启后就有概率丢数据因为这个模式下事务提交时不强制刷redo log数据还停留在日志缓冲区里。这个实验我建议每个DBA和业务开发都亲手做一次它对理解“持久性不是数据库宕机保护而是由刷盘时机决定的”特别有帮助。真到了生产事故排查时你一眼就能根据参数判断出是配置问题还是硬件故障。6.3 我的一些体会做了这么多年MySQL排查和优化我最大的感受是很多问题如果只停留在SQL语法和索引层面永远治标不治本。连接器、优化器、InnoDB内存结构、日志落盘策略它们是一条完整的链路。你理解了Buffer Pool和脏页刷盘就能明白为什么大量无索引查询不只是慢还会拖垮整个实例的IO你理解了redo log和binlog的两阶段提交就能判断主从延迟的根源到底在传输层还是刷盘层你理解了undo log的版本链就再也不会写一个长事务把undo表空间撑爆。如果让我给刚接触MySQL的同学一个落地建议我会说先把这条链路画出来再往每个节点填参数和命令。连接器对应SHOW PROCESSLIST优化器对应EXPLAINBuffer Pool对应状态变量日志机制对应双1参数。等你把这几个节点串成一条线很多“玄学”问题其实都是透明的。
返回列表