别只优化慢查询:MySQL生产六大高危故障排查

作者:问题终结者2026.11.06 22:28浏览量:0

简介:生产环境真正把工程师半夜叫醒的,往往不是慢查询,而是连接打满、长事务持锁、隐式死锁、元数据锁阻塞、升级后连不上库、主从延迟这六类故障。本文按“现象—定位—根因—修复—预防”五步逐项拆解,配可直接执行的排查命令与落地建议,帮你把数据库故障处置从被动救火升级为主动防御。

在数据库稳定性这个话题上,存在一个常见的认知错位:团队把大量精力投入慢查询治理,索引一个个建、执行计划一个个看,但真正让系统半夜出事、让业务大面积抖动的,几乎从来不是慢查询。慢查询是渐进式的,有充足的优化窗口;而生产环境的高危故障,连接突然打满、锁莫名其妙不释放、升级之后应用集体连不上库,往往不报任何语法错误,常规日志里也看不出头绪,等摸到根因,业务可能已经抖了一个小时。本文整理六类在生产一线反复出现的高危故障,每一类都按照“现象、定位、根因、修复、预防”五步展开,可以直接当作排障手册来用。

一、连接打满:数据库负载不高,应用却集体报错

现象:应用突然大量报 Too many connections,新请求全部失败;但登录数据库一看,CPU 只有 20% 左右,负载并不高,谁都连不进去。

定位:执行下面两条命令,快速判断连接的“量”与“质”:

  1. SHOW GLOBAL STATUS LIKE 'Threads_connected';
  2. SHOW PROCESSLIST;

如果连接数已经贴近 max_connections,而 SHOW PROCESSLIST 的结果里大片是 Sleep 状态,基本可以确认:空闲连接堆积,把配额耗光了。

根因:这类故障通常由两个因素叠加。其一是服务端回收太慢:wait_timeout 默认是 8 小时(28800 秒),一条空闲连接要等 8 小时才被回收;其二是应用侧行为失控:没有使用连接池、每次请求都新建连接,高峰期形成短连接风暴,而 8.0 版本 max_connections 的默认值只有 151,很容易瞬间打满。还有一种隐蔽情况——网络闪断或应用异常重启后,连接在服务端残留成“僵尸 Sleep 连接”,持续占坑。

修复:

  1. -- 收紧空闲回收时间(只对之后新建的连接生效,重启后失效)
  2. SET GLOBAL wait_timeout = 300;
  3. -- 确认连接上没有未提交事务后,清理堆积的空闲连接
  4. KILL <连接ID>;

两点提醒:SET GLOBAL 是临时修改,重启即失效,长期生效必须写入配置文件并重启实例;KILL 之前务必确认目标连接不在事务中,否则可能把用户的操作拦腰截断。

预防:所有应用统一走连接池,并且连接池上限必须小于数据库的 max_connections,给运维和突发场景留出余量。连接池也不是越大越好,数据库能高效服务的活跃连接数,受 CPU 核数与磁盘 IO 能力约束,盲目调大连接池,只是把排队从应用挪到了数据库,问题并没有消失。

二、长事务:一条 UPDATE 卡住,全表业务陪葬

现象:某张表的 UPDATE 全部超时,报 Lock wait timeout exceeded(错误码 1205),业务持续重试、持续失败。

定位:

  1. SELECT trx_id, trx_state, trx_started, trx_rows_locked, trx_mysql_thread_id
  2. FROM information_schema.innodb_trx
  3. ORDER BY trx_started;

按事务开始时间排序,大概率躺着一条 RUNNING 状态、已运行数小时、锁行数巨大的事务。它握着锁不撒手,后面所有写同一批数据的请求只能排队等超时。

根因:最常见的成因是代码开了事务忘了提交——异常分支没回滚,finally 里也没有兜底。更隐蔽的成因是连接池“接盘”:上一个请求异常退出时,带着半截事务的连接被原样归还到池里,下一个请求取出的还是这条连接,锁也一并继承过来。还有一类是事务里掺杂了远程调用:一次下游接口超时几秒,数据库锁就多握几秒,并发一上来就是连锁反应。

修复与预防:

  • 把事务边界写死:开启事务的方法里用 try/finally 保证“要么提交、要么回滚”,任何路径都不泄漏事务;
  • 事务内只做数据库操作,远程调用、消息发送、文件处理一律挪出事务;
  • 连接归还前强制回滚未完成事务,这是连接池接入层的基础职责;
  • 加监控:对 innodb_trx 中运行超过 30 秒的事务告警,长事务越早暴露代价越小。

长事务比慢查询更危险的地方在于“传染性”:慢查询只拖慢自己,长事务会让所有依赖相同数据的业务一起变慢,排队连接持续累积,最终还会演变成第一节讲的连接打满,故障叠加,排查难度翻倍。

三、唯一键冲突“假死锁”:代码没加锁,死锁日志一堆

现象:应用大量报 Deadlock found when trying to get lock(错误码 1213),开发翻遍代码也没找到显式加锁的逻辑,一度怀疑数据库“抽风”。

定位:

  1. SHOW ENGINE INNODB STATUS;
  2. -- 重点看 LATEST DETECTED DEADLOCK 段

如果死锁日志里,互相等待的几个事务都在对同一张表执行 INSERT … ON DUPLICATE KEY UPDATE,且等待关系是“持有共享锁、等待排他锁”,根因就基本锁定。

根因:INSERT … ON DUPLICATE KEY UPDATE 这条语句在执行时,会先对唯一键加共享锁检查冲突,需要更新时再升级为排他锁。当大量并发请求同时写同一条唯一键记录时,两个事务可能各自持有共享锁、又都在等对方释放才能加排他锁,形成典型的锁升级死锁。它和代码写没写锁无关,是语句本身的加锁时序在高并发下的固有风险。

修复思路:

  • 拆开写:先查再写(SELECT 判断存在性,再决定 INSERT 或 UPDATE),加锁路径更清晰;并发下仍可能撞上重复键错误(错误码 1062),应用层按“冲突即重试”兜底;
  • 对 1213 做自动重试:死锁被检测后,InnoDB 会主动回滚代价较小的事务,重试大概率能成功;重试几次仍失败再升级告警;
  • 设计层打散热点:计数、去重这类高频写,改为分桶累加或异步合并,避免所有请求挤在同一行上。

四、元数据锁(MDL):一条 ALTER 把整张表冻住

现象:发版时执行一个 ALTER TABLE,结果整张表读写全卡死,连最普通的 SELECT 都不返回;数据库 CPU 却低得很,所有线程都在等锁,没有人在干活。

定位:

  1. SELECT * FROM sys.schema_table_lock_waits;
  2. -- 也可查询 performance_schema.metadata_locks 观察锁的持有与等待关系

典型的等待链条是:某个长查询或未提交事务持有该表的 MDL 读锁 → ALTER 申请 MDL 写锁,进入排队 → MDL 队列讲究先来后到,后面所有新来的 SELECT、UPDATE 全部排在 ALTER 之后。于是整张表瞬间冻结。

根因:很多人以为是 ALTER 直接锁表,真相是 ALTER 先被别人挡住,然后挡住了所有人,它是受害者,也是放大器。真正的源头,是那个跑了几小时的长事务或慢查询。

修复:先 kill 阻塞 ALTER 的那个事务,表立刻恢复。注意顺序:如果先 kill ALTER,长事务继续跑,表虽然恢复,但下一次变更还会在同一个坑里摔一遍。

预防:

  • DDL 前先查 innodb_trx 与 processlist,确认目标表上没有长事务、长查询;
  • 给变更会话设置 lock_wait_timeout(例如 5 秒),拿不到锁快速失败,不要长时间排队;
  • 大表结构变更走在线 DDL 或业界通用的在线变更方案,分批推进;
  • DDL 安排在业务低峰执行,并提前公告,留出应急预案。

五、升级后应用连不上:认证插件与字符集的“兼容暗坑”

现象:数据库版本升级顺利完成,监控一切正常,但应用全部连接失败,错误类似”Authentication plugin ‘caching_sha2_password’ cannot be loaded”,或握手阶段的 SSL 异常。

定位:先用与应用同版本的驱动手工连一次,复现报错;再检查服务端配置:

  1. SHOW VARIABLES LIKE 'default_authentication_plugin';
  2. SELECT user, host, plugin FROM mysql.user WHERE user = '<业务账号>';

根因:8.0 起,默认认证插件换成了 caching_sha2_password,握手流程与旧插件不同,并默认要求加密通道或额外的密钥交换支持;老版本驱动既不认识新插件、也不支持新的握手流程,连接阶段直接失败。此外,新版本默认字符集与排序规则也有调整,跨版本迁移或混用场景下,涉及排序规则的 SQL 可能报错,这类问题往往在升级后才集中爆发。

修复:

  • 根治:升级应用侧驱动/连接组件到支持新认证协议的版本;
  • 缓解:为业务账号显式指定旧认证插件,给驱动升级争取时间:
  1. ALTER USER '<业务账号>'@'%' IDENTIFIED WITH mysql_native_password BY '<密码>';

预防:版本升级必须在测试环境完成“数据库 + 应用 + 驱动 + 中间件”的全链路验证,不能只验证数据库本身;默认插件、字符集、排序规则的差异要提前列成核对清单;灰度阶段保留回滚窗口,出问题能分钟级切回。

六、主从延迟:大事务把复制链路拖垮

现象:读从库的应用读到旧数据,或延迟保护逻辑导致大量请求强制回主库。SHOW REPLICA STATUS(旧版本为 SHOW SLAVE STATUS)里,Seconds_Behind_Source(旧字段名 Seconds_Behind_Master)持续增长,回放线程追不上。

根因:主从复制的本质是“主库记日志、从库回放”,延迟通常来自三类源头。一是大事务:主库上批量更新百万行的事务,从库回放要按同样粒度串行执行,回放期间延迟持续累积,曲线呈“锯齿状”;二是回放并行度不足:并行复制没开或配置过保守,写入洪峰一来就跟不上,延迟呈“持续爬升”;三是从库自身瓶颈:回放是大量随机写,从库磁盘 IO 或回放线程 CPU 打满,硬件规格与主库不对等是常见诱因。

定位与修复:

  • 先看延迟曲线形态:锯齿状指向大事务,持续爬升指向能力不足,对症下药;
  • 开启并合理配置并行回放(worker 数量、基于 WRITESET 或逻辑时钟的依赖追踪),提升回放并行度;
  • 治理大事务:批量订正类任务强制拆批,例如每批千行、批次间隔秒级执行;
  • 核对从库硬件规格,重点确保磁盘与主库对等,并监控回放线程的资源占用。

预防:把从库延迟纳入核心监控,区分“突发”与“持续”两种形态分别告警;读写分离与故障切换逻辑中内置延迟保护,延迟超阈值强制读主库,避免“读到旧数据”这类静默错误。

结语:从被动救火,到主动防线

复盘这六类故障,可以提炼出共性规律:它们都不是孤立的点状问题,而是“服务端默认配置 + 应用侧行为 + 变更操作”三者叠加的结果。wait_timeout 太宽、事务边界不严、DDL 撞上长事务、驱动没跟上认证协议、大事务冲垮回放,每条链路单独看都“还好”,叠起来就是半夜的事故。

把救火升级为防线,建议分三层推进:

  • 资源层:连接数、磁盘空间、复制延迟、事务时长等核心指标设置水位告警,在“打满”之前预警;
  • 行为层:长事务、大事务、热点写入纳入发布前评审与代码规范,从源头减少危险操作;
  • 变更层:DDL 与版本升级建立标准动作,事前检查长事务与兼容性清单,事中低峰灰度,事后复盘并把教训固化到流程里。

如果使用的是云数据库托管服务,例如百度智能云上的云数据库实例,通常内置了连接数、锁等待、主从延迟等维度的监控与告警能力,可以直接复用,把告警体系建设得更省力。

慢查询优化当然值得坚持,但它只是数据库稳定性工程的一小块拼图。把上面六类问题的“现象—定位—根因—修复—预防”真正吃透,生产环境的数据库才谈得上“守护”,而不是“听天由命”。