当前位置: 首页 > news >正文

餐饮外卖点餐小程序源码性能优化实录(附代码)——Redis 热点缓存、接口限流与数据库索引设计

餐饮外卖小程序看起来技术门槛不高,但真正扛住午高峰的并发,才知道坑在哪。我们的项目日均 8000 单,高峰期 90 分钟内涌入 5000+ 请求,上线第一周就被打崩:菜品接口响应近 5 秒,下单超时率 12%,数据库 CPU 长期顶在 95%。复盘下来,问题就三个:热点数据全打数据库、接口没有限流保护、SQL 查询压根没建索引。这篇文章不讲理论,只记录我们怎么一个一个解决这些问题。方案全部在生产环境跑过,核心代码直接贴出来,能复用的你直接拿走。

源码与演示:c.ymzan.top

Redis 热点缓存:解决"所有人都在查同一批菜"

1. 问题分析

点餐小程序最典型的场景是:几百人同时打开首页,查看同一家店的菜品列表。此时数据库会收到大量重复查询:

SELECT*FROMdishWHEREshop_id=101ANDstatus=1ORDERBYsort_desc;

这条 SQL 在高峰期每秒被执行 200+ 次,但数据本身 5 分钟内不会变。

2.方案:本地缓存 + Redis 二级缓存

我们采用了Caffeine 本地缓存(L1)+ Redis(L2)的两级架构:

  • L1:单实例缓存,命中后无需任何网络开销,适合极端热点
  • L2:分布式缓存,保证多实例间数据一致

关键代码

@ServicepublicclassDishService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;// Caffeine 本地缓存,最大 500 条,过期时间 60 秒privatefinalCache<Long,List<DishVO>>localCache=Caffeine.newBuilder().maximumSize(500).expireAfterWrite(60,TimeUnit.SECONDS).build();privatestaticfinalStringDISH_CACHE_KEY="dish:shop:";privatestaticfinallongCACHE_TTL=300;// Redis 缓存 5 分钟publicList<DishVO>getDishList(LongshopId){// L1:本地缓存List<DishVO>cached=localCache.getIfPresent(shopId);if(cached!=null){returncached;}// L2:Redis 缓存Stringkey=DISH_CACHE_KEY+shopId;cached=(List<DishVO>)redisTemplate.opsForValue().get(key);if(cached!=null){localCache.put(shopId,cached);// 回填 L1returncached;}// 缓存未命中,查数据库cached=dishMapper.selectByShopId(shopId);if(!cached.isEmpty()){redisTemplate.opsForValue().set(key,cached,CACHE_TTL,TimeUnit.SECONDS);localCache.put(shopId,cached);}returncached;}// 商户修改菜品时,同时清空 L1 和 L2publicvoidupdateDish(Dishdish){dishMapper.updateById(dish);redisTemplate.delete(DISH_CACHE_KEY+dish.getShopId());localCache.invalidate(dish.getShopId());}}

3. 热点 key 的额外处理

午餐高峰时,头部商家(日均 500+ 单)的菜品查询会成为热点 key,所有请求都打到同一个 Redis 节点。

我们的解法是:key 加随机后缀分散访问。在写入缓存时:

// 写入时加随机后缀Stringkey=DISH_CACHE_KEY+shopId+":"+ThreadLocalRandom.current().nextInt(10);// 读取时尝试 10 个后缀for(inti=0;i<10;i++){Objectval=redisTemplate.opsForValue().get(DISH_CACHE_KEY+shopId+":"+i);if(val!=null)return(List<DishVO>)val;}

这让原本集中在一个节点的请求分散到了 10 个 key 上,Redis 节点 CPU 从 80% 降到了 15%。

优化效果:菜品列表接口 P99 从 4.8s 降到 120ms。

接口限流:防止恶意请求和突发流量击穿系统

1. 问题分析

除了正常高峰流量,我们还遇到过两类问题:

  1. 前端重复提交:用户网络卡顿时疯狂点击"下单",同一订单被提交 5-8 次
  2. 恶意刷接口:有人用脚本高频调用菜品查询接口,挤占正常用户资源

2. 方案:Guava RateLimiter + Redis 分布式限流

单机限流用Guava RateLimiter,分布式场景用Redis + Lua 脚本

单机限流(下单接口)

@ComponentpublicclassOrderRateLimiter{// 每秒只允许 100 次下单privatefinalRateLimiterrateLimiter=RateLimiter.create(100.0);publicbooleantryAcquire(){returnrateLimiter.tryAcquire(50,TimeUnit.MILLISECONDS);}}// 在 Controller 中使用@PostMapping("/order/create")publicResultcreateOrder(@RequestBodyOrderDTOdto){if(!orderRateLimiter.tryAcquire()){returnResult.fail("系统繁忙,请稍后重试");}// 正常下单逻辑...}

分布式限流(菜品查询接口,Redis + Lua)

publicbooleantryAcquire(StringuserId,StringapiPath,intmaxRequests,intwindowSeconds){Stringkey="rate_limit:"+apiPath+":"+userId;StringluaScript="local current = redis.call('INCR', KEYS[1]) "+"if current == 1 then "+" redis.call('EXPIRE', KEYS[1], ARGV[1]) "+"end "+"return tonumber(current) <= tonumber(ARGV[2])";Longresult=redisTemplate.execute(newDefaultRedisScript<>(luaScript,Long.class),Collections.singletonList(key),windowSeconds,maxRequests);returnresult!=null&&result==1L;}

限流规则我们设为:

接口限流阈值窗口
菜品查询30 次/用户/分钟滑动窗口
下单创建5 次/用户/分钟固定窗口
商户后台查询100 次/IP/秒滑动窗口

优化效果:重复下单率从 8% 降到 0.3%,恶意刷接口被完全拦截。

数据库索引设计:让慢查询消失

1. 问题分析

优化前,我们用EXPLAIN分析了下单高峰期最慢的三条 SQL:

SQL扫描行数执行时间
查询用户近 30 天订单86,0002.1s
查询商家今日销量120,0003.4s
查询骑手待取餐订单45,0001.8s

全表扫描,没有任何有效索引。

2. 索引优化方案

原则:索引不是越多越好,只建高频查询需要的联合索引。

① 订单查询优化(用户侧 + 商户侧)

-- 用户查询自己的订单:user_id + create_time 联合索引CREATEINDEXidx_order_user_timeON`order`(user_id,create_timeDESC);-- 商户查询今日订单:shop_id + create_time 联合索引CREATEINDEXidx_order_shop_timeON`order`(shop_id,create_timeDESC);-- 骑手查询待取餐:status + assign_time 联合索引CREATEINDEXidx_order_status_timeON`order`(status,assign_time);

② 菜品表优化

-- 原来只有主键索引,查询时全表扫描-- 新增:shop_id + status 联合索引(覆盖最常见的查询条件)CREATEINDEXidx_dish_shop_statusONdish(shop_id,status,sort_desc);-- 查询菜品详情:id 主键已够,无需额外索引

③ 避免索引失效的坑

我们踩过一个典型的坑:原来有idx_order_user_time索引,但查询时用了WHERE user_id = ? AND DATE(create_time) = CURDATE(),导致索引失效。

修复方式是改写 SQL,避免在索引列上使用函数:

-- ❌ 索引失效WHEREDATE(create_time)=CURDATE()-- ✅ 索引生效WHEREcreate_time>=CURDATE()ANDcreate_time<CURDATE()+INTERVAL1DAY

优化效果:三条慢查询全部降到 50ms 以内,数据库 CPU 从 95% 降到 30%。

结语

回过头看,这次优化本质上就做了三件事:用两级缓存扛住热点读,用双层限流挡住异常流量,用精准索引消灭慢查询。没有引入复杂的中间件,没有重构架构,但效果立竿见影——P99 从 4.8 秒降到 120 毫秒,数据库 CPU 从 95% 回落到 30%。当然,优化没有终点。后来我们又陆续处理了缓存击穿、索引失效、连接池配置等问题,但这三板斧解决了 80% 的性能瓶颈,是性价比最高的第一步。如果你的小程序也在高峰期卡顿,建议先从这三个方向查起,别一上来就想着重构。

http://www.jsqmd.com/news/1077230/

相关文章:

  • 低功耗IoT设备电源管理:PMIC选型与i.MX RT600系统设计实践
  • 线性回归实战指南:从面试陷阱到工业级诊断与部署
  • 7 大 AI Agent 平台深度技术横评:Coze、Dify、百炼、360智语、千帆、Copilot Studio、LangGraph 政企选型全拆解
  • 【撕开黑盒学大模型】划清玩具与生产级系统的边界:LLM Agent 的稳定性、可观测性与生态解耦思辨
  • 3步实现输入法词库无缝迁移:告别平台切换的困扰
  • Diffusers实战指南:Stable Diffusion生产级部署与调优
  • BilldDesk:免费开源的跨平台远程桌面解决方案完全指南
  • Sqribble深度解析:模板驱动的文档操作系统架构
  • 计算机毕业设计之“速餐”校园订餐系统的设计与实现
  • 全网资源下载神器res-downloader:5分钟学会智能抓取视频音频
  • 加权AM-GM不等式:从乘积极值到线性优化的降维策略
  • 如何将 iPad 同步至新电脑,且不丢失原有数据?
  • 3步掌握Flowframes:让你的视频帧率翻倍的终极AI工具
  • 2026甘肃考公机构梯队排名:从第一梯队到潜力机构,哪家更值得选?
  • 顶刊聚焦|肿瘤相关巨噬细胞(TAM)新的功能亚群 —— 机制已解构,空间待解析
  • 工业级遗传算法实战:问题驱动的GA工程化落地指南
  • 2026免费一键去图片水印的app有哪些:无广告手机软件与跨平台选择指南
  • 大型洗涤厂必看!一套好用的布草管理系统应具备哪些功能?
  • vscode到底有什么用
  • 生产级ML模型部署:从Notebook到稳定推理服务
  • VMware虚拟机Java开发环境配置失效?——20年经验总结的6类隐蔽性Host-Only网络陷阱及修复时间表
  • Winlator终极指南:3步搞定Android上的Windows应用输入控制
  • Cesium 渐变色墙体教程
  • 微创介入是“矛“,中医扶正是“盾“——杭州这家医院把两者融成了一体
  • 号码认证哪家好?关键指标与权威平台推荐
  • INT8量化实战:从FP32模型到边缘端高效推理的完整工程链
  • iOS自动化测试核心:WebDriverAgent原理、配置与Appium集成实战
  • 两种并发模式
  • 当“散装物料”遇上“智慧装车”:工厂里的装车,也可以很智能
  • 国内冷镦钢厂主要分布在哪些产区?