第一页很快,第两千页却卡住;数据库 CPU 不高,接口 P99 仍然上升。这样的慢,往往不是“机器不够用”这么简单。
订单系统同时要服务用户最近订单、商家待处理列表和运营跨店检索。把这些访问方式塞进一个万能查询,再加几个单列索引,数据增长之后很容易暴露问题。
我更愿意按“请求耗时在哪里 → 数据库读取了多少 → 返回内容是否必要”的顺序处理,再决定要不要引入缓存和分片。
先区分 SQL 慢,还是接口慢
一条接口链路至少要区分连接池等待、SQL 执行、结果传输、对象映射、远程调用和 JSON 序列化。日志里只记录 Controller 总耗时,无法判断瓶颈在哪。
如果慢在连接池排队,盲目扩大连接数可能进一步压垮数据库;如果订单列表为每一行再查一次商家和商品,那是 N+1 查询,单条 SQL 的执行计划再漂亮也救不了整条链路。
排查时保留查询模板、租户维度、筛选条件、返回行数和 trace ID。不要把完整用户信息或支付凭据直接打进慢日志。不同时间段、不同数据倾斜下的耗时,要分开观察。
联合索引从访问方式长出来
假设常用页面查询某商家某状态下的最近订单,排序为创建时间倒序,时间相同时用订单 ID 打破平局:
1 | CREATE INDEX idx_order_merchant_status_created_id |
等值过滤字段在前,排序字段在后,是这条查询的候选索引设计;是否有效仍要看真实执行计划和数据分布。若商家 ID 只在租户内唯一,租户字段也要进入查询与索引前缀,不能把权限隔离寄托在前端。
当页面改成“全部状态”,status 的条件消失,原索引未必还能直接提供期望的全局时间排序。此时应把它当作另一种查询模板分析,而不是宣称一个联合索引能覆盖所有搜索组合。
SELECT * 也不是无关紧要的细节。订单备注、地址、扩展 JSON 等大字段,会增加回表与网络传输成本。列表只取展示字段,详情按需查询,通常比缓存整个订单对象更容易控制。
深分页是在反复丢弃数据
LIMIT 100000, 50 需要找到并跳过前面的大量记录,结果却只返回 50 行。对于连续向后浏览的页面,可以用上一页最后一条记录作为游标:
1 | SELECT id, created_at, amount_minor, status |
首次请求不带游标条件,后续游标同时携带时间和 ID,且数据库时间精度不能在序列化过程中被截断。只用时间会在同一时刻创建的订单之间漏数据;只用 ID 则可能与实际排序语义不一致。
游标还应绑定筛选条件和授权范围,服务端每次都重新校验权限。筛选变化后继续使用旧游标,得到的结果没有可靠含义。
这条方案换来的是稳定的连续翻页,不是任意跳到第 N 页。状态在翻页期间变化,仍可能影响列表成员;需要严格一致的导出时,应使用固定数据快照或专门导出任务,不能把游标分页误当成快照隔离。
执行计划要看“做了多少”,不只看“用了索引”
先用 EXPLAIN 看访问路径和估算,再在受控环境用 EXPLAIN ANALYZE 对照实际行数、循环次数与耗时。后者会实际执行查询,不能直接拿重型查询在生产高峰随意试。MySQL EXPLAIN 文档
重点关注几个差距:估算行数与实际行数相差多少;为了返回 50 行读取了多少行;是否出现昂贵排序或临时结果;关联表是否被反复访问。
“Using filesort” 本身不是必须消除的故障,小结果集排序可能完全可以接受。反过来,走了一个选择性很差的索引,也可能比合理扫描更慢。优化目标是降低整体成本,而不是让执行计划的关键词好看。
缓存和分片,各自解决不同的问题
缓存适合可接受短暂陈旧的数据,比如商家统计摘要。订单支付状态、退款进度等用户正在追踪的内容,要明确失效策略和读取权威来源的时机。缓存键必须包含租户、筛选条件和必要的权限维度。
引入分片前先列查询矩阵:
| 访问方式 | 分片后需要回答的问题 |
|---|---|
| 用户最近订单 | 能否携带分片键直接定位? |
| 商家待处理订单 | 用户维度分片是否导致全分片扫描? |
| 根据订单号查详情 | 订单号能否路由,或是否有查询索引? |
| 跨店经营报表 | 是否应该进入独立分析读模型? |
| 支付回调与退款 | 唯一约束与事务边界是否还在同一分片? |
ShardingSphere 能帮助路由和改写 SQL,但不能替你决定订单应按哪个业务维度组织。跨分片排序、分页和全局唯一约束,都会增加新的维护成本。
容量增长也不只是分片数量。每个分片都有连接、备份、迁移、DDL 和恢复成本。冷数据归档、合理索引和减少无意义查询应先完成,避免把同一条低效 SQL 复制到更多库上。
怎样证明这次优化值得保留
对照测试需要保持数据规模、数据分布、并发、页大小、查询条件和硬件一致,同时记录缓存冷热状态。只拿一次热缓存请求和一次冷缓存请求比较,不能说明 SQL 优化了多少。
观察 P50、P95、P99、错误率、扫描行数、连接池等待与数据库负载;新增索引还要测订单写入和更新成本。若读接口改善,却让订单状态更新更容易锁等待,方案仍需要调整。
优化后的说明应该能回答:减少了哪一类工作,付出了什么代价,在哪种流量下重新评估。这样的结论,比一个缺少测量条件的“性能提升数倍”更有复用价值。
延伸阅读:支付消息如何可靠到达订单系统。