百万订单统计从3秒压到30毫秒,我是怎么一步步把慢接口救回来的

简介: 微服务, spring cloud, spring boot, java, 教程, springcloud alibaba, nacos, sentinel, seata, 后端开发

上个月加了个周六的班,就为一个接口。

运营后台的订单统计页,P99耗时稳定在2.8秒,高峰期冲到4秒开外。运营点个"今日GMV"要转圈半天,老板看数据大屏卡了一次,直接在群里@技术,问是不是服务器没续费。

讲真,这种活最不讨喜。业务跑得好好的没人记得你,卡了全是你的锅。

但捋下来发现问题比想象中有意思——不是简单"加个索引"就能搞定,而是从数据库选型、缓存策略到对账补偿的一整套方案。最终接口从2.8秒干到30毫秒,今天把整个过程拆给你看,下次遇到类似场景直接抄作业。

先说结论:不是SQL写得烂,是思路从根上错了

还原一下现场。那个接口是典型的运营统计需求:按时间范围、渠道、状态、商品分类等维度,统计订单数、GMV、客单价、退款率,前端还带分页明细。

原来的写法最朴素——前端传参,Service拼SQL,MySQL执行count、sum、group by,MyBatis映射成VO返回。逻辑看着没毛病,代码也干净。

问题出在数据量。订单主表500多万行,每天还在以几万条增长。运营最爱干什么?选最近30天,不选状态,按分类group by,再加个分页导出。一条SQL扫几十万行,在临时表里排序分组,磁盘临时表都给你跑出来,不慢才怪。

第一反应也是"加索引"。

冷静下来想,这治标不治本。统计型查询和业务型查询不一样。业务查询是"找某条具体数据",索引能精准命中;统计查询是"算一堆数据的汇总",就算建了覆盖索引,InnoDB的count(*)依然要把匹配到的每一行扫一遍逐行计数,几十万行就是几十万次IO。多维度任意组合的情况,索引建少了不顶用,建多了写入维护成本爆炸。

本质问题是:你在用MySQL干它不擅长的活——实时大批量聚合计算。

想明白这点,方案就清晰了:把"查询时实时算"改成"写入时同步算",也就是预聚合。

方案选型:为什么最终选了MySQL+Redis

动手之前先列了几种思路,逐个对比。

SQL优化+索引:短期有效,长期无解。组合维度一多索引根本覆盖不过来,count(*)本身就是InnoDB弱项。放弃。

离线T+1跑批:每天凌晨汇总前一天数据到统计表,但运营要看"今日实时GMV",T+1满足不了实时性。放弃。

ClickHouse/StarRocks这类OLAP引擎:性能确实猛,但为了一个后台统计页面就上新中间件,运维成本和数据同步复杂度都上去了,我们团队小,扛不起。搁置。

Redis预聚合:订单创建、支付、取消、退款时,顺手把对应维度的计数器在Redis里加一。查询端直接读计数器,毫秒级返回。缺点是Redis挂了数据会丢,需要补偿。

最终选的是Redis预聚合+MySQL兜底的组合架构:

  • MySQL作为唯一数据源,订单可靠落库,任何时候都能重建Redis里的统计
  • Redis作为加速层,所有实时统计走预聚合计数器,查询只读Redis
  • 每天凌晨定时全量重建,用MySQL重算指标刷新Redis
  • 对账任务周期对比MySQL和Redis值,偏差过大自动重建

"实时增量为主,全量重建兜底"。既拿到Redis的毫秒级响应,又不至于Redis一挂统计就瘫痪。

Key设计:预聚合成也Key败也Key

做Redis预聚合,Key设计是灵魂。随手写死一个字符串,后期维度一加你就崩。

我用的规则:stat:order:{维度标识}:{维度值}:{指标名}:{时间粒度}:{时间值}。

举个例子,"2026年10月18日app渠道已支付订单"的Key:

stat:order:channel:app:status:paid:cnt:day:20261018\
stat:order:channel:app:status:paid:gmv:day:20261018

为什么把指标名(cnt/gmv/refund_cnt)也拼进去?因为不同指标数据类型不一样——计数用INCR,金额用INCRBYFLOAT。拆成独立Key,写的时候互不影响,读的时候一次mget就行。

时间粒度做了三层:day、hour、minute。天报表按day,实时大屏看minute。小时和天粒度靠定时任务从分钟粒度roll up。

一个必踩的坑:维度枚举一定要预先收敛,不能让用户随便传值当Key后缀。恶意请求传个随机字符串当channel,Redis瞬间多出几十万个无用Key,内存直接爆。我在代码里做了白名单校验,不在枚举里的维度直接拒绝。

核心实现:双写没你想的简单

关键动作就是在订单状态变更节点——创建、支付、取消、退款——同步更新Redis计数器。

听着就是几个INCR?没那么简单。

第一个坑:计数一定要幂等。 支付回调可能重复投递,MQ也可能重复消费。每次收到"已支付"都INCR,重复消费一次就多算一笔。我基于订单ID做去重:已处理过的订单ID存入Redis SET,处理前先判断。SET只保留最近7天ID,内存可控。

第二个坑:增减要成对。 订单从"待支付"变"已支付",要给paid_cnt加一、unpaid_cnt减一。最开始只加不减,待支付数量一直涨,数据一塌糊涂。后来抽象通用方法,传入旧状态和新状态自动算加减,业务代码只管调。

第三个坑:浮点数精度。 GMV用INCRBYFLOAT累加,多次累加后可能出现19.990000000000002这种尾差。读取时统一用BigDecimal做ROUND_HALF_UP保留两位小数即可。

核心增量逻辑(伪代码):

public void onOrderStatusChange(OrderEvent event) { \
// 1. 幂等去重\
String dedupKey = "stat:order:dedup:" + event.getOrderId();\
Boolean isNew = redisTemplate.opsForValue()\
.setIfAbsent(dedupKey, "1", 7, TimeUnit.DAYS);\
if (Boolean.FALSE.equals(isNew)) return;\
\
// 2. 旧状态减一,新状态加一\
String day = DateUtil.format(event.getOccurTime(), "yyyyMMdd");\
updateCounter(event, event.getOldStatus(), -1, day);\
updateCounter(event, event.getNewStatus(), 1, day);\
}\
\
private void updateCounter(OrderEvent e, int status, int delta, String day) { \
String cntKey = String.format(\
"stat:order:channel:%s:status:%d:cnt:day:%s",\
e.getChannel(), status, day);\
redisTemplate.opsForValue().increment(cntKey, delta);\
if (status == OrderStatus.PAID.getCode()) { \
String gmvKey = String.format(\
"stat:order:channel:%s:gmv:day:%s", e.getChannel(), day);\
redisTemplate.opsForValue()\
.increment(gmvKey, delta * e.getAmount().doubleValue());\
}\
}

代码不复杂,关键是所有状态流转入口都要调到,漏一个数据就歪。

深分页顺手也治了

原接口除了统计慢,分页明细也慢。LIMIT 100000, 20 这种写法,MySQL先扫10万行扔掉再取20行,越往后越慢。运营导出数据翻到几千页是常事。

我用游标分页替换offset分页。前端不传pageNo,传上一页最后一条记录的id,WHERE改成:

WHERE id < #{lastId} ORDER BY id DESC LIMIT 20

每次走主键索引,从lastId往后扫20行就完事,翻到第100页和第1页速度一样。缺点是不能跳转到任意页,但运营后台"上一页/下一页"完全够用,这个取舍值得。

兜底:Redis不能成单点

缓存方案最怕什么?Redis挂了,或者某个计数器bug算错了你没发现,运营拿着错数据开周会,那就是生产事故。

做了三层兜底:

本地Caffeine二级缓存:每个统计Key在应用本地缓存10秒。Redis真挂了,10秒内的请求还能顶着,给你留熔断反应时间。

全量重建任务:每天凌晨2点跑任务,用MySQL把前一天所有维度的统计值重算一遍覆盖Redis。相当于每天对一次账,把MQ丢失、服务重启、主从切换等偏差修正掉。

实时对账监控:每15分钟随机抽几个维度,用MySQL实时count和Redis对比。偏差超1%就告警并自动重建该维度。这样就算漏了双写,15分钟内就能发现。

上线后对账跑了一周,最大偏差0.3%,原因是个边界case:订单23:59:59创建、00:00:03支付跨天了。后来把时间维度按业务发生时间取、而非事件处理时间取,就修好了。

最终效果

上线一周后的数据:

  • P99耗时:2.8秒 → 32毫秒,提升约87倍
  • 高峰期P99:4.2秒 → 48毫秒
  • MySQL统计类慢SQL:每天几百条 → 清零
  • Redis预聚合Key总内存:不到20MB
  • 运营投诉:零。还有运营特意跑过来问"是不是换服务器了,怎么突然这么快"

最后几个提醒

预聚合不是银弹。 维度组合爆炸时(几十个维度任意组合),Key数量会呈笛卡尔积增长,Redis内存可能扛不住。这种场景要么上OLAP,要么只预聚合TOP N高频维度组合,剩下的回源。我这次维度不多(渠道5个、状态6个、分类10个出头),组合数可控才敢这么干。

别在事务里写Redis。 最开始我把Redis更新放在MySQL事务提交前,结果事务回滚了,Redis已经加一,数据对不上。正确做法是在事务commit成功后的回调里发MQ,由消费者异步更新Redis。

计数器用String不用Hash。 一开始图省事想把同一天所有维度塞进一个Hash,HINCRBY一把搞定。后来发现Hash在field多时会触发rehash,单次操作耗时陡增,也没法单独给某个field设过期时间。拆成独立String Key,性能稳,过期也好控制。

过期时间要设对。 天粒度Key设90天过期,够运营查季度数据;分钟粒度只设3天过期,不然内存越积越多。别图省事全设永久,跑半年Redis就满了。

写在最后

这次优化最大的感受:性能问题到了一定阶段,拼的已经不是SQL调优技巧了,而是你愿不愿意换个思路。

遇到慢查询,很多人第一反应是EXPLAIN、加索引、改SQL。这些都没错,但如果数据量和查询模式本身超出了MySQL的舒适区,在SQL层面再怎么抠,收益也是线性的。换一个更适合的组件或组合,收益是数量级的。

但也别为了炫技上一堆花里胡哨的中间件。上ClickHouse、上Flink做实时数仓当然也行,那是杀鸡用牛刀。Redis+MySQL这套组合,两个人周改完,线上跑了两个月稳如老狗,这就够了。

技术选型从来不是选最牛的,是选最合适的。

你们线上有没有遇到过那种"怎么调都调不快"的接口?最后用什么方案解决的?评论区聊聊,我攒着下次写。


小坏说Java一个只说真话的程序员博主 2026年10月19日

相关文章
|
6天前
|
云安全 人工智能 运维
当攻击全面Agent化,你的安全防御能否跟上AI Speed?
多起客户侧AK泄露事件应急的真实观察
1290 6
|
4天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
|
9天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
29天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9868 33
|
15天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
6天前
|
Web App开发 人工智能 JavaScript
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
REA(Reverse Engineer Anything)是近期爆火的开源AI逆向工具,4月发布,1个月内斩获15万+ Star。它让AI Agent无需源码即可分析二进制、APK、JS/Electron、HAR包等17类目标,自动反编译、生成可运行代码,并强制返回带证据与局限的结论。全本地执行,MIT协议,支持Hopper/Ghidra/IDA等引擎,一行命令即可上手。
694 2
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
|
4天前
|
人工智能 开发者
Qoder CN 大使计划正式开启|无限创造,等你一起
全年免费积分、优先内测、活动支持与更多共创机会,邀请用 Qoder 把想法变成现实的你。
465 0
|
14天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
992 1