资讯动态

MySQL性能优化实战:从慢查询定位到架构设计

发布时间:2026/10/13 7:55:08 来源:尧图企业网站定制
1. 从一条慢查询说起这个系列到底想讲什么1.1 一个真实到有点尴尬的深夜先说个发生在我自己身上的事。某天凌晨两点线上告警群突然炸了某业务接口的 TP99 从 200ms 一路飙到 3 秒。我睡眼惺忪地打开慢查询日志发现罪魁祸首是一条看起来人畜无害的 SQLSELECT * FROM orders WHERE user_id 1024 ORDER BY created_at DESC LIMIT 20;这表当时有 3000 万行user_id 上确实建了索引。可问题就出在SELECT *和ORDER BY created_at的组合上MySQL 需要先根据 user_id 找到几千条记录再对这几千条记录做一次临时表的排序最后才取 20 条。如果运气不好user_id 命中的人特别多再叠加写入高峰慢查询就直接把连接池拖垮了。那晚之后我意识到一件事数据库优化不是靠某一条“神级 SQL”或者一个“万能参数”就能解决的它是一套从现象到根因、从设计到验证的完整方法。这也是我决定写这个系列的最初动机。1.2 MySQL 优化到底在优化什么很多人一听到“MySQL 优化”第一反应是“调几个参数”或者“加个索引”。其实这只是最表层的东西。你把问题想深一层就会发现MySQL 优化本质上是在做四件事减少磁盘 I/O。数据要从磁盘读到内存内存不够还要和磁盘频繁换入换出I/O 一旦成为瓶颈再快的 CPU 也白搭。减少排序和临时表。排序、去重、分组都会在内存或磁盘上生成临时结果集这些操作极其昂贵。减少锁竞争和事务等待。并发上来之后行锁、间隙锁、元数据锁会把请求排队排到怀疑人生。减少不必要的网络和日志开销。比如返回了根本用不到的字段、binlog 刷得太频繁、每条 SQL 都多传了不该传的数据。换句话说优化目标不是“让某条 SQL 变快”而是“让整个数据库在既定硬件和业务负载下吞吐更高、延迟更稳、可预期性更强”。这个系列的每一篇都会围绕这些目标展开不会只丢给你几个参数就完事。1.3 这个系列适合谁看、应该怎么用如果你是刚入行的后端开发经常被 DBA 或者导师说“这条 SQL 太差了”那这个系列能帮你建立起排查问题的基础框架知道从哪看起、怎么分析、怎么改。如果你已经写了两三年业务代码开始接手一些性能优化工作那你不仅能拿到具体手段还能看到每个方案背后的取舍和适用边界。即使你是 DBA想整理一套给研发团队看的优化规范这个系列里的排查方法和避坑清单也可以直接拿来当参考。我建议你不要只是收藏。最好的办法是手边准备一套测试环境导入一份真实业务脱敏数据跟着文章里的步骤实际跑一遍。优化这件事光看永远不会踩过坑才算真的会。2. 性能问题定位先搞清楚瓶颈在哪再动手2.1 用户感知和数据库指标怎么对上号优化最忌讳的就是“还没确诊就开药”。很多同学一上来就问“MySQL 的 CPU 跑到 90% 了怎么办”但你要知道CPU 高只是表象背后可能是全表扫描可能是频繁排序也可能是大量逻辑读导致的行锁竞争。所以在动手之前先要把业务现象翻译成数据库指标。接口变慢优先看慢查询日志、innodb_row_lock_current_waits、Threads_running。数据库卡死重点看连接数、线程并发、锁等待、redo log 写入情况。偶尔抖动需要结合监控看是否和备份、大事务、DDL、缓存失效时间点重合。主从延迟关注 Seconds_Behind_Master、binlog 堆积量、从库的刷盘压力。我做优化时有个习惯先拉出最近一周的时间线把接口延迟曲线、CPU、I/O、慢查询数量、连接数叠到一张图上。很多时候问题根本不是“某个值太高”而是“某个时间段内各指标一起突变”。比如凌晨的定时任务和大促流量撞在一起那单纯调索引也救不回来。2.2 慢查询日志最原始的破案线索慢查询日志是排查性能问题最直接的入口。先确认它有没有打开别笑很多生产实例默认就是关的。SHOW VARIABLES LIKE slow_query_log; SHOW VARIABLES LIKE long_query_time;如果没开可以通过参数临时打开SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;long_query_time我建议从 1 秒起步先看一个周期的日志量再决定要不要收紧到 500ms。设置太大等于没设太小容易把日志刷爆。另外提醒一句log_queries_not_using_indexes在生产环境要慎开因为某些全表扫描的查询其实是合理的比如数据量极小的字典表真开了会让日志量爆炸。拿到慢查询日志之后别直接抄 SQL 去 EXPLAIN。先做聚合分析看看哪些 SQL 出现的次数最多、累计执行时间最长。同一个模子的 SQL 往往只是参数不同要抽象出模板再分析。我用过不少工具但实际上自己写个脚本按“去掉具体值的 SQL 指纹”聚合往往最灵活。2.3 用 EXPLAIN 读懂执行计划定位到候选 SQL 之后就该看执行计划了。这是每个优化者必须掌握的基本功。EXPLAIN SELECT * FROM orders WHERE user_id 1024 ORDER BY created_at DESC LIMIT 20;重点看几个字段type从 const、ref、range 到 index、ALL访问效率越来越差。ALL就是全表扫描能避免尽量避免。key实际用到的索引。明明建了索引但 key 是 NULL那就是索引没生效。rows估算扫描的行数。这个数字和真实行数差距很大时统计信息可能过期了可能要ANALYZE TABLE。Extra出现Using temporary或者Using filesort时就要警惕。filesort 不代表磁盘文件排序它也可能在内存排序但排序这个动作本身是有成本的。多说一句EXPLAIN是估算值不是真实执行值。对于一些复杂 SQL我有时候会改用EXPLAIN ANALYZEMySQL 8.0它能给出实际执行时间和各步骤耗时定位更精准。如果生产环境不方便用也可以在测试环境先导入数据再跑。2.4 性能基线和日常巡检没有基线就没法判断“变慢了多少”。我见过很多团队上线前不压测上线后凭感觉说“系统变慢了”可具体慢多少、慢在哪里谁也说不清。我的建议是至少给核心接口和核心 SQL 建立三层基线接口层TP50、TP95、TP99按业务维度统计。数据库层QPS、TPS、慢查询数量、活跃连接数、临时表创建速率。资源层CPU、内存、磁盘 I/O、网络带宽。巡检频率可以分两级每天自动巡检一次把关键指标和前 7 天对比每周人工看一眼趋势重点排查那些“缓慢爬坡”的指标比如临时表数量逐日上涨、缓冲池命中率逐步下降。这类趋势问题最阴不会立刻炸但会在某一天突然要你还债。3. 索引优化大多数慢查询的症结都在这3.1 索引为什么快为什么也会失效索引的本质是换一个数据结构去减少数据扫描量。InnoDB 的索引用的是 B 树叶子节点有序存储查询从原来的“扫全表”变成“按路径查找”复杂度从 O(n) 降到 O(log n)。你可以把它理解成一本字典的目录正常查字不会从第一页翻到最后一页而是先翻目录、再定位页码。但索引不是银弹。设计得不好它带来的可能不是加速而是负担。因为每次 INSERT、UPDATE、DELETE 都要同步修改索引索引越多写放大越严重。所以“能不加索引就不加需要加才加”才是正确心态。索引失效更是高频问题。最典型的一种对索引列做了函数运算或隐式类型转换。比如WHERE DATE(created_at) 2024-01-01这种写法会让 created_at 上的索引失效因为 MySQL 要对每一行都先算一遍 DATE 才能比较。正确的做法是改写范围条件WHERE created_at 2024-01-01 00:00:00 AND created_at 2024-01-02 00:00:003.2 联合索引的设计原则与常见误区联合索引是面试和实战里的常客也是坑最多的地方。它的核心原则叫“最左前缀原则”意思是查询条件必须从联合索引的最左列开始连续命中索引才会被用到。举个例子表上有联合索引(user_id, status, created_at)WHERE user_id 1024 AND status 1能用索引。WHERE status 1 AND user_id 1024MySQL 优化器会做调整也能用索引。WHERE status 1不走索引因为跳过了最左列 user_id。WHERE user_id 1024 AND created_at 2024-01-01能走索引但只用到 user_id 这一列created_at 上的索引条件可能无法继续用于范围检索之后的精确定位。设计联合索引时我的经验排序是区分度高的列放前面其次是把经常用于等值查询的列放前面最后才是范围查询列。别盲目把每个查询条件都塞进同一个索引里不然索引会越来越宽存储和写入成本都会上升。更好的办法是统计业务真实查询挑出高频的组合再去设计而不是拍脑袋决定。3.3 索引失效场景自查清单我整理了这些年在实际代码 review 里最常见的索引失效场景基本覆盖了 90% 的问题对索引列使用函数或表达式如WHERE id 1 10。隐式类型转换比如索引列是字符串却传入数字比较。使用LIKE %关键词这种左模糊匹配。OR 连接的多个条件里有一个列没建索引。联合索引不满足最左前缀。优化器认为全表扫描比走索引更划算常见于小表或统计信息严重不准。前五条都好理解最后一条最容易被忽略。优化器判断要不要走索引是基于估算行数和成本模型。如果表的统计信息没更新或者数据分布极度不均匀比如某个值占了 90% 的行它可能做出“反直觉”的决定。这时候要做的不是强制FORCE INDEX而是先ANALYZE TABLE更新统计信息再考虑改写 SQL。3.4 覆盖索引与回表少一次查询就多一分快InnoDB 的二级索引只保存索引列和主键。如果查询需要的字段不在索引里就得根据主键再到聚簇索引里取一次完整行记录这个过程叫“回表”。回表本身不一定是坏事但扫描行数多时回表次数也会跟着多I/O 消耗就上去了。覆盖索引的思路就是让查询需要的所有列都包含在某个索引中这样二级索引本身就能满足查询不用回表。比如你经常用user_id查status和created_at那联合索引(user_id, status, created_at)就是一个覆盖索引。注意这里有个权衡为了覆盖而把很多宽字段塞进索引会让索引体积暴涨反而不划算。一般只对高频且字段量少的查询做覆盖优化。如果你看到执行计划里的Extra显示Using index就说明这条查询已经被覆盖索引完全接住了这是很理想的状态。4. SQL 写法优化换个写法快十倍不是玄学4.1 SELECT * 的隐藏成本很多开发者写SELECT *纯粹是图方便但它有两个问题。第一返回了业务根本不需要的列白白增加网络传输时间和内存占用。第二如果这些列不在索引中就会迫使 MySQL 回表去取完整记录覆盖面越大代价越高。尤其当表里有一两个大字段比如 JSON、TEXT时SELECT *很可能把一个本来很快的索引查询拖成重度 I/O 查询。正确的姿势是先看业务到底需要哪些字段只查用到的那几列。这不是让你“每列都要点名”而是要有意识避免无脑全取。配合覆盖索引优化时这一点尤其重要。4.2 深分页问题LIMIT 1000000, 20 为什么这么慢分页接口最常见的写法是SELECT * FROM orders ORDER BY created_at DESC LIMIT 1000000, 20;这个写法的执行逻辑是扫描前 1000020 行然后丢掉前 1000000 行只把最后 20 行返回。翻得越深扫描和丢弃的行越多性能呈线性甚至更差。解决办法有两种。第一种叫“游标分页”前提是排序字段唯一且有序SELECT * FROM orders WHERE created_at 2024-05-01 00:00:00 ORDER BY created_at DESC LIMIT 20;前端拿到上一页最后一条记录的排序字段值下一页带上这个值继续查。这种方案对无限下拉的 App 很合适。第二种是“延迟关联延迟 join”SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY created_at DESC LIMIT 1000000, 20 ) t ON o.id t.id;子查询里只查主键并做排序扫描的字段宽度很窄效率比全行扫描高很多。我实测过百万级深分页这种写法能快一个数量级。4.3 JOIN 和子查询别迷信任何一种网上有两种声音一边说子查询性能差一边说 JOIN 容易产生临时表。我的观点是没有绝对优劣关键看执行计划和数据量。小数据量子查询可能没问题大表 JOIN 时必须注意驱动表的选择和连接字段的索引。实际排查中我更在意的是JOIN 的关联字段有没有索引。比如订单表和用户表做 JOIN如果关联字段 user_id 在用户表上没有索引那每匹配一条订单都要去用户表做一次全表扫描这是灾难。通常做法是让驱动表尽量小被驱动表的连接列尽量有索引。另外分页配合 JOIN 时强烈建议先分页再 JOIN而不是 JOIN 全部结果再分页。也就是先查出一个 page 的主键集合再去关联其他表这能显著减少大结果集的组合数量。4.4 IN、EXISTS 和 OR 的取舍先说 OR。它最容易被优化器折腾如果 OR 连接的多个条件中任何一个涉及的列没有可用索引MySQL 可能放弃索引转而全表扫描。这种 SQL 通常可以改成 UNION ALL让每一段都能独立走索引。再说 IN 和 EXISTS。IN 适合外表大、内表小的情况EXISTS 适合外表小、内表大的情况。但 MySQL 优化器在 5.6 之后做了一些半连接优化很多时候会把 IN 改写成 EXISTS 或其他形式。所以与其死记规则不如直接看EXPLAIN确认是不是用了合适的执行策略。另外要注意 IN 列表太长导致索引结构反复查找的开销一般超过几百个值建议分拆或者改写 JOIN。还有一个不易察觉的坑IN (NULL)或者列表中包含 NULL 会导致结果语义变化处理不当容易查出“不该出现”的数据这个属于逻辑正确性问题比性能更值得警惕。5. 配置文件与参数调优改参数前先理解这些5.1 InnoDB 缓冲池最重要的一块内存InnoDB 的数据读写并不是直接和磁盘打交道而是先经过缓冲池Buffer Pool。读数据时如果数据页已经存在于缓冲池就直接返回不存在才去磁盘读取并放入缓冲池。写数据也一样先修改缓冲池中的页再由后台线程慢慢刷回磁盘。所以 Buffer Pool 越大数据页在内存中的命中率越高磁盘 I/O 就越少。一般建议把这个值设为物理内存的 60% 到 75%具体要看服务器上是否还有其他服务在跑。设置之后别忘了观察命中和读写情况SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read%; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_pages%;Innodb_buffer_pool_read_requests是总读取次数Innodb_buffer_pool_reads是从磁盘读取的次数两者相除就能估算命中率。如果命中率低于 99%说明缓冲池可能偏小或者数据访问模式存在大量冷读。MySQL 8.0 里还可以把缓冲池设置成多个实例用innodb_buffer_pool_instances控制减少大并发下的内部锁竞争。改这个参数要重启实例需要提前做好变更窗口和回滚方案。5.2 关键参数怎么调、哪些不建议乱动很多刚接触优化的同学喜欢在网上找“最佳配置”直接粘贴这种做法我不赞同。数据库配置和业务模型强相关没有放之四海而皆准的数字。但有几个参数是值得每个实践者去理解的innodb_flush_log_at_trx_commit。这个参数决定事务提交时 redo log 的刷盘策略。默认 1 是每次提交都刷盘最安全但最慢设成 2 表示提交时只写操作系统缓存每秒才刷一次盘性能提升明显但异常断电可能丢 1 秒内的日志。在线业务、账务相关的数据别乱调宁可性能差一点。sync_binlog。和innodb_flush_log_at_trx_commit配合着看安全组合同样是 1性能损耗最大想要更高性能可以设置成 N 或 0但主从切换、异常断电时可能出现数据不一致的风险。innodb_io_capacity和innodb_io_capacity_max。这组参数控制后台刷页的速率机械硬盘和 SSD 的设定逻辑完全不同设太低会导致脏页堆积设太高又会干扰正常业务。最好结合磁盘的实际 IOPS 能力做压测确定。max_connections。这个值不是越大越好连接数多了会带来大量线程上下文切换。更稳妥的做法是控制应用层的连接池规模配合数据库端监控超出合理水位就告警。不太建议随手改的参数包括innodb_buffer_pool_size之外的各类“高并发魔法参数”比如innodb_thread_concurrency、innodb_spin_wait_delay。很多人只看到别的团队改完效果不错却不知道别人的硬件规格、业务类型和压测样本是什么。你把一堆参数堆上去可能只换来微小的提升却引入更多不确定性。5.3 连接数、事务隔离级别与锁等待连接数问题往往是“压垮数据库的最后一根稻草”。应用层连接池配置过大、请求突增、慢 SQL 占用连接不释放三者叠加会直接把线程池打满。用一个简单公式估算连接池大小建议控制在 CPU 核心数乘以 2 再加一点余量的水平。如果核心的 SQL 都是快查询连接池没必要设到 100如果有一些不可避免的重查询连接池再大慢查询也会把它占满还不如把慢查询优化掉。事务隔离级别方面大多数业务用默认的 REPEATABLE READ可重复读问题不大但它会产生更多的间隙锁。如果你对一致性要求没那么高很多报表类、统计类场景可以改成 READ COMMITTED显著降低某些锁等待和死锁概率。这个改动是全局性的需要和业务一起评估。锁等待排查常用这几条语句SHOW ENGINE INNODB STATUS; SELECT * FROM performance_schema.data_lock_waits; SELECT * FROM sys.innodb_lock_waits;看到锁等待事件后先定位持锁事务跑了多久、涉及哪些 SQL。我见过很多死锁都是“两条 SQL 对同一批数据按不同顺序加锁”导致的比如事务 A 先更新订单再更新用户事务 B 先更新用户再更新订单。这种问题靠调参数解决不了必须改业务代码把加锁顺序统一起来。5.4 参数修改后的验证方法改参数最怕的就是“改完没验证上线就出事”。我的标准流程是先在测试环境按生产流量模型压测对比修改前后的 TP99、CPU、I/O 和锁等待。确认没问题后再在生产环境做灰度变更一次只改一个参数改完观察至少一个业务高峰周期。要注意有些参数是动态的用SET GLOBAL马上生效但重启后失效有些参数写在配置文件里必须重启。动态生效的参数也要记得同步到配置文件避免下次重启被还原成旧值。这个细节我踩过不止一次。6. 架构层面的优化单实例到头了怎么办6.1 读写分离读多写少业务的起步方案当单库的读压力已经让 CPU 和 I/O 持续走高读写分离通常是第一反应。核心思路很简单主库负责写从库负责读通过 binlog 把数据变化同步到从库。但读写分离的难点不在“搭”而在“一致性”。主从复制是异步的刚写入主库的数据在从库上可能还查不到。如果业务对实时性要求高比如“下单之后立刻看到订单详情”这种查询如果落到从库就会读到旧数据。常见的处理方式有关键读流量强制走主库比如用户自己的订单列表。接受短时间延迟在页面做状态提示。用半同步复制或组复制把不一致窗口压小但代价是写入性能会受影响。我之前接手过一个系统读写分离做好之后接口性能确实提升明显但偶然出现“用户反馈刚改的密码登录不了”的问题就是因为修改密码的写走主库登录校验读走了从库复制延迟了那么几百毫秒。最后是把登录校验改成优先读主库才解决。这类坑设计阶段就要想清楚。6.2 分库分表能不分就不分分了就别回头分库分表是优化手段里成本最高的一个应该排在其他手段都试过之后再考虑。因为它会带来分布式 ID、跨库查询、事务一致性、扩容复杂性等一系列新问题。这里我给出几个判断条件单表数据量过大索引维护成本和查询性能明显下降。写入吞吐超过单库能力CPU、I/O 持续在高位。预估未来数据增长会继续超过硬件扩容的性价比边界。分片键的选择是重中之重。绝大多数场景用“用户 ID”做分片键因为业务查询天然以用户维度切入。比较难受的是按时间分表之后跨月或跨年的聚合查询会变成“扫多张表再合并”这类需求如果很重就需要评估是不是应该上分析型存储或数据仓库。还有一点分库分表之后尽量把所有查询都带上分片键否则就会走广播查询把全部库表都访问一遍。无法带分片键的查询就要考虑引入映射表或者搜索引擎。这块不是一朝一夕能做完的最好在数据量还没爆掉的时候提前规划。6.3 缓存层挡在 MySQL 前面的第一道防线缓存和 MySQL 不是二选一而是配合关系。最理想的状态是 MySQL 只承担“最终一致性”的落库职责大量重复读流量被缓存挡住。常见的缓存策略有 Cache Aside、Read Through、Write Through 几种但实践中用得最多的是 Cache Aside。以用户信息查询为例先查缓存命中就直接返回没命中就查 MySQL然后把结果写回缓存并返回。更新时先更新数据库再删除缓存而不是更新缓存。为什么是“删缓存”而不是“更新缓存”因为更新缓存在并发场景下容易出现新老数据交错并且如果缓存里的值需要经过复杂计算得到那每次更新都算一遍太浪费。删除缓存虽然第一次读会 miss但能避免很多一致性隐患。用缓存最怕的是“缓存雪崩”和“缓存击穿”。要么给过期时间加随机抖动避免大量 key 同时过期要么用互斥锁或热点 key 永不失效策略保护那些频繁访问的单一数据。好的缓存设计不是让命中率变成 100%而是在极致流量下保证绝大多数请求不被打到 MySQL。6.4 数据归档与冷热分离很多表的性能问题不是 MySQL 不行而是“热数据被冷数据拖累”。比如订单表积累了三五年历史数据大部分旧订单不会再被访问但它们依然占据空间、参与统计信息计算、拖慢索引维护。最有效的办法就是归档。把超过一定时间的数据从主表搬到归档表或者导出到其他存储。归档方案要提前想好两个问题一是归档任务的执行不能影响在线业务建议分批进行每批控制在几千条以内二是归档后的数据如果未来要查询必须有清晰的查询路径归档表在不在线库、能不能通过老接口查到这些都要跟业务约定清楚。我的经验是冷热分离之后主表体积变小索引高度变低查询性能往往能有一个档次的提升。但要注意归档不是把数据丢掉它是一套从在线库到离线存储的数据生命周期管理流程需要有监控、补偿和恢复机制。7. 常见问题与排查技巧实录7.1 索引明明建了SQL 还是很慢遇到这种问题第一步先别怀疑 MySQL先检查执行计划。常见情况SQL 里对索引列做了隐式转换。比如表里 user_id 是 varchar传入的是数字MySQL 会尝试把列转成数字再比较索引就失效了。查询条件写的顺序和联合索引不一致但你以为一致。统计信息过期优化器估算偏差大。用ANALYZE TABLE更新后重新 EXPLAIN。数据分布极度倾斜。某个值命中了几百万行优化器算下来还是全表扫描更“划算”但不代表业务可以接受。这时要么改查询语义要么考虑按业务再分层索引。我在某张表上遇到过一种情况加了索引还慢后来发现是 WHERE 条件里有个OR另一列没索引优化器干脆放弃全部索引。改成 UNION ALL 之后直接从 8 秒降到 300ms。这类问题光看表结构是看不出来的必须看到完整 SQL。7.2 测试环境快生产环境慢“测试环境明明几百毫秒生产就要几秒”这种反馈我听得太多了。根因往往不在 SQL而在环境和数据差异生产数据量远大于测试环境索引基数、统计信息分布完全不同。生产环境的 Buffer Pool 被大量业务数据占满测试环境的缓存冷却度低。生产环境的并发写和锁等待测试环境根本模拟不出来。生产环境的硬件可能在高峰期被其他业务抢占尤其是虚拟化环境。排查路径是先在生产库上用EXPLAIN对比执行计划看扫描行数是不是差了很多再看数据库当时的活跃会话和锁等待最后压测环境尽量用生产脱敏数据的抽样集而不是造几条假数据。数据规模差一个数量级很多结论都会被推翻。7.3 连接数被打满应用疯狂报超时连接池打满通常不是突然发生而是“慢 SQL 拖着连接不放”导致的恶性循环。一条慢 SQL 跑了 10 秒就意味着这个连接被占用了 10 秒当慢 SQL 并发到几十条连接池就瞬间被消耗完后续请求全部排队超时。处理步骤先杀掉明显异常的慢查询释放连接恢复服务。抓取慢查询日志定位哪些 SQL 在高峰期集中出现。检查应用层连接池配置maxTotal 和 maxWait 是否合理。优化 SQL 和加索引让单条查询耗时降下来。配置熔断和降级策略当慢查询数量超过阈值时拒绝部分非核心请求。比较实操的建议是给应用层连接池设置一个比数据库 max_connections 小很多的峰值预留一些连接给 DBA 和管理工具使用。否则应用层把连接用完后DBA 想连上去杀进程都连不进去。7.4 主从延迟越来越大主从延迟的成因很多常见的有这么几类从库硬件比主库差或者承担了大量读流量刷盘和 redo 应用能力不足。主库大事务比如一次 UPDATE 影响几十万行这个事务的 binlog 传到从库后要执行很久。从库上存在慢查询和锁等待导致 SQL 线程被阻塞。从库复制是单线程应用在高并发写入场景下容易跟不上。排查时先用SHOW SLAVE STATUS看 Seconds_Behind_Master同时对比主库的 binlog 写入量和从库的执行进度。如果是大事务导致只能拆分事务分批提交。如果是从库读压力太大建议在从库前面加一层缓存或者增加更多只读从库做负载均衡。这里还要注意Seconds_Behind_Master 在某些情况下可能是 0但不代表数据完全一致比如从库的 SQL 线程正在执行一个大事务时这个值无法反映真实差距。最好再对比一下主从的 binlog 点位或者 GTID 集合。7.5 死锁不是调参数能解决的死锁的本质是两个或多个事务互相持有对方需要的锁形成循环等待。InnoDB 检测到死锁会回滚其中一个事务让另一个继续执行。应用层看到的现象就是某个事务突然报错。定位死锁最直接的办法是SHOW ENGINE INNODB STATUS;看LATEST DETECTED DEADLOCK部分里面会记录两个事务执行的最近 SQL 和持有的锁信息。绝大多数死锁的解决方案是调整业务代码里的加锁顺序比如约定所有事务都先操作订单表再操作用户表。实在改不动代码的情况下可以缩短事务执行时间减少锁持有范围降低死锁概率但根治还是要靠顺序一致。我在实际项目里还总结过一个经验审查死锁的时候别只看报错的那条 SQL要把它所在的事务里所有 SQL 都翻出来看。死锁往往不是单条 SQL 的问题而是整个事务的锁集合和别的锁集合交叉。8. 最后分享几点我的真实体会优化 MySQL 这件事我一直觉得最难的其实是“克制”。知道加索引很容易知道调大内存参数很容易但知道什么时候不该加索引、什么时候不该改全局参数才是真正考验经验的地方。一个成熟的优化方案应该是业务收益、维护成本、风险边界三者平衡的结果而不是把某个指标调到最好看。我个人的习惯是每次优化完都会在文档里记三个东西优化前现象、优化做了什么、优化之后指标变化。还会备注一个“如果下次再出现类似问题先看什么”。三个月之后回看这些记录你会发现自己对问题的理解深度完全不一样。如果你想快速提升建议选一套测试环境把本系列提到的方法全部走一遍构造慢查询、定位执行计划、设计索引、改写 SQL、调整参数、模拟主从延迟。中间必然会踩坑但那些坑才是真正属于你的经验。下一篇我会详细拆解一个从慢查询到索引设计的完整案例包括具体的数据规模、SQL 变化和执行计划对比到时候见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑