外观
第二次线上事故:一条 SQL 慢查询引发的连锁
约 1157 字
第二次线上事故:一条 SQL 慢查询引发的连锁
第一篇事故复盘(在新窗口打开)讲的是 「头像集体变默认」,这次更典型:一条没走索引的 慢查询,拖垮了整个数据库,引发服务雪崩。 数据脱敏,过程和教训是真的。
一、事故现象
周四晚高峰,监控连环报警:
19:02 API 响应时间 P99 从 200ms 飙到 8s
19:05 数据库 CPU 100%,活跃连接打满
19:08 订单服务、用户服务、支付服务全部超时
19:10 客服进线爆炸:「下单转圈」全站下单功能瘫痪 25 分钟。晚高峰, GMV 损失六位数。复盘会上我屁股全程没离开椅子。
二、时间线
![这次没画图,用文字时间线]
18:45 运营上线「订单搜索优化」功能(加了个按备注模糊搜索)
19:02 慢查询开始堆积:SELECT * FROM orders WHERE remark LIKE '%keyword%'
19:05 数据库连接池(上限 50)被打满
19:08 所有依赖数据库的服务排队等连接 → 雪崩
19:10 告警通知到值班群
19:12 回滚功能,慢查询消失
19:15 连接池恢复,服务自愈
19:20 全部正常从上线到炸,17 分钟。从炸到恢复,10 分钟。
三、根因链
表面是慢查询,实际是三颗雷叠着踩:
雷一:前置通配的 LIKE
SELECT * FROM orders WHERE remark LIKE '%手机%'%手机% 前后都通配,B+ 树索引直接失效 (索引是按前缀组织的,前缀通配等于没前缀)。 百万级订单表全表扫描,单条 4-6 秒。
(原理在MySQL 索引(在新窗口打开)那篇, 「常见索引失效」一节第一条。)
雷二:没 LIMIT + SELECT *
一条搜索返回可能几万行,还带所有字段。 内存、网络、序列化全线吃紧。
雷三:连接池太小 + 无隔离
连接池 50,一条慢查询占着连接不还, 50 条慢查询进来,池满。其他正常请求 也排在后面等连接 —— 一个功能拖垮全站。
四、正确的做法
当时应该怎么写这个功能:
-- 1. 加 LIMIT 和分页
SELECT id, order_no, amount FROM orders
WHERE remark LIKE '%手机%'
LIMIT 20 OFFSET 0;
-- 2. 高并发搜索上搜索引擎(ES),别硬刚 MySQL
-- 3. 过渡期:功能降级,搜索不可用时给提示以及架构上:
- 慢查询隔离:搜索类请求走独立连接池 / 独立实例,别和交易混用;
- 超时兜底:SQL 执行超 1 秒直接掐断 (
max_execution_time),别让它占着连接; - 上线前 SQL review:涉及全表扫描的, DBA 或老鸟过目。
五、复盘:四个教训
教训一:新功能上线 = 新风险上线 这次没人把「搜索」当高危功能, 但它动了核心表。规则要改: 任何新查询,先 EXPLAIN 看执行计划 —— 看到 type: ALL(全表扫描)就不许上。 (本站 MySQL 那篇的 EXPLAIN 章节, 就是这次之后补的。)
教训二:连接池是公共资源,要有隔离 交易链路和非交易链路混用连接池, 等于「快递和急救车抢一条道」。 搜索、报表这类「重但非核心」的请求, 给单独的数据源。
教训三:监控要「分层报警」 这次数据库 CPU 90% 时没报警, 等打满才响 —— 晚了。分级阈值 (70% 预警、85% 严重、95% 紧急)
- 慢查询日志监控,能把发现时间 从 8 分钟压到 1 分钟。
教训四:回滚要快,但要「带着问题上滚」 这次 2 分钟回滚,恢复了服务 —— 但如果只是回滚了事,下次换个功能照样炸。 回滚是止损,复盘才是止血。 这次之后加的规矩:
上线前:新查询必须过 EXPLAIN
运行中:慢查询日志实时监控 + SQL 超时熔断
故障时:分级告警 + 一键回滚脚本六、后记
现在我自己写涉及数据库的功能, 第一反应是 EXPLAIN —— 这个肌肉记忆 是六位数的 GMV 换来的。
两次事故(头像(在新窗口打开)和这次) 共同点:都不是高深技术问题, 是「觉得不会出事的低级问题」。 高危场景里没有「小事」—— 全表扫描、默认密码、无备份, 每一个都觉得「应该没事」,每一个都上过新闻。
与诸位程序员共勉:敬畏生产环境。