大宗商品销采运储系统・六层架构连载(数据持久层)
上一篇我们通过冷热分离,把历史数据这个"包袱"甩掉了,让热库的查询重新跑得飞快。
但很多时候,拖垮数据库的并不是数据量有多大,而是某几个业务人员跑出的"烂 SQL"。
在大宗商品系统里,动辄几百个微服务、几千个接口,只要有一个接口写了全表扫描,或者发生了隐式类型转换,它就能在瞬间吃光数据库的 CPU,把连接池打满,导致其他几百个正常接口全部超时。
这就是典型的"一颗老鼠屎坏了一锅粥"。
架构做得再好,分库分表再牛,也顶不住一条烂 SQL 的降维打击。
一句话定调:
慢 SQL 治理不靠"事后救火",靠的是"事前监控、事中阻断、事后复盘"的闭环机制。
一、大宗商品系统里,慢 SQL 都是从哪来的?
排查了无数个线上故障,大宗商品的慢 SQL 重灾区永远集中在以下三个地方:
1. 深分页与全表扫描
-
反面教材 :
SELECT * FROM t_order WHERE customer_name LIKE '%西安XX%'; -
死因 :前置
%导致索引失效,千万级大表直接全表扫描。或者深分页LIMIT 1000000, 10,导致 MySQL 扫描大量无用数据并频繁回表。
2. 隐式类型转换
-
反面教材 :
order_no是VARCHAR(32)类型,但代码里传了数字类型WHERE order_no = 12345678; -
死因:MySQL 会把字符串转成数字比较,直接导致 B+ 树索引失效,变成全表扫描。
3. 大表 JOIN 与复杂统计
-
反面教材 :
SELECT ... FROM t_order o LEFT JOIN t_order_detail d ON o.id = d.order_id LEFT JOIN t_customer c ... WHERE ... GROUP BY ... ORDER BY ... -
死因:多张大表 Join,如果在被驱动表上没有合适的索引,会产生极其可怕的笛卡尔积,直接吃满内存(Using temporary / Using filesort)。

二、事前监控:让慢 SQL 无处遁形
不能等业务投诉系统卡了再去抓,必须让慢 SQL 自动报警。
1. MySQL 慢查询日志(基础防线)
在 MySQL 配置中强制开启慢查询日志:
[mysqld]# 开启慢查询日志slow_query_log = 1# 慢查询日志文件路径slow_query_log_file = /var/log/mysql/slow.log# 阈值:超过 1 秒记录(大宗商品核心交易建议设置为 0.5 秒)long_query_time = 1# 记录没有使用索引的 SQLlog_queries_not_using_indexes = 1# 记录管理语句(如 ALTER TABLE)log_slow_admin_statements = 1
2. Prometheus + MySQL Exporter(告警闭环)
结合公共中间件层的可观测体系,使用 mysqld_exporter 采集慢查询指标,配置 Alertmanager 告警规则:
- alert: MySQLSlowQueriesHigh expr: rate(mysql_global_status_slow_queries[5m]) > 5 for: 2m labels: severity: P1 annotations: summary: "MySQL 慢查询突增(当前每秒 {{ $value }} 条)"
3. Druid 连接池监控(应用侧防线)
如果使用 Spring Boot + Druid,开启内置的监控面板,可以实时看到应用发出的 SQL 执行时间、并发数、执行计划。
三、事中诊断:用 EXPLAIN 一眼看穿死穴
抓到慢 SQL 后,第一件事就是加 EXPLAIN,看执行计划。
EXPLAIN SELECT * FROM t_sale_order WHERE status = 1 AND customer_name = '西安XX贸易';
核心字段解读(避坑指南):
| 字段 | 警告标志 | 含义与解法 |
|---|---|---|
| type | ALL, index |
表示全表扫描。必须优化到 ref(非唯一索引)、range(范围查询)或 eq_ref(主键/唯一索引)。 |
| key | NULL |
表示没走索引。检查是否没建索引,或发生了索引失效(如前置%,隐式转换)。 |
| rows | 数值极大 | 预计扫描的行数。如果几百万行,绝对是灾难。 |
| Extra | Using filesort |
表示需要额外排序。应通过建立合适的联合索引来消除排序。 |
Using temporary |
表示使用了临时表(常见于 GROUP BY 或 DISTINCT)。极易撑爆内存。 |

四、事后复盘:SQL 优化的三板斧
第一斧:索引优化(最有效)
-
联合索引的最左前缀原则 :如果建了
(status, create_time, customer_id)索引,查询时条件必须按顺序带头使用status。 -
覆盖索引 :如果查询只需要主键和状态,尽量
SELECT id, status,避免SELECT *,可以直接在索引树上取数据,无需"回表"查主键。
第二斧:SQL 改写
-
禁止
SELECT *。 -
避免在索引列上做函数计算 :
WHERE DATE(create_time) = '2026-10-10'会让索引失效。必须改写为WHERE create_time >= '2026-10-10 00:00:00' AND create_time <= '2026-10-10 23:59:59'。 -
小表驱动大表:在 Join 时,尽量用数据量小的表作为驱动表。
第三斧:业务妥协(架构兜底)
- 如果实在优化不了(比如几十万行数据的实时多维度汇总),不要死磕 MySQL。利用我们在中间件层讲的 Elasticsearch 做异构检索,或者通过调度服务做 T+1 离线预计算。
五、终极防御:HikariCP 连接池调优
很多时候,数据库其实没死,只是几个慢 SQL 把应用端(Java)的连接池打满了,导致后续的所有线程都在 Connection Timeout 排队等待。

Spring Boot 默认使用 HikariCP,大宗商品高并发场景下的标准配置:
spring: datasource: hikari: minimum-idle: 10 maximum-pool-size: 50 # 核心公式:(CPU核心数 * 2) + 有效磁盘数 idle-timeout: 600000 # 空闲连接超时时间(10分钟) max-lifetime: 1800000 # 连接最大存活时间(30分钟) connection-timeout: 3000 # 获取连接超时时间(3秒,超时直接报错,防止拖死线程) validation-timeout: 1000 # 连接有效性检测 connection-test-query: SELECT 1
⚠️ 连接池防雪崩核心机制
如果某接口发生慢 SQL,占满 50 个连接,后面的请求会在 3 秒后 connection-timeout 报错。
必须配合 Sentinel 做限流降级 :
针对已知会慢的报表接口,单独限制并发(比如只允许 5 个并发查库),防止它把整个连接池抢光。
@SentinelResource(value = "exportOrderReport", blockHandler = "handleBlock")
public byte[] exportReport(ParamDTO dto) {
// 大查询...
}
六、本篇小结
慢 SQL 治理是一场持久战。
记住四句话:
-
监控先行:开启慢查询日志,配好 Prometheus 告警。
-
诊断靠 Explain :关注
type、key和Extra,杜绝全表扫描。 -
优化三板斧:建联合索引、改写函数计算、消灭隐式转换。
-
连接池要限流:HikariCP 设好超时,Sentinel 兜底防雪崩。
下篇预告:AI 赋能 DBA:SQL 智能改写、索引推荐与故障根因分析。探讨如何利用大模型,把以前必须靠资深 DBA 人工看的慢 SQL 日志,变成自动化的优化建议?