尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

餐饮外卖点餐小程序源码性能优化实录(附代码)——Redis 热点缓存、接口限流与数据库索引设计
📅 发布时间:2026/6/25 21:24:58

餐饮外卖小程序看起来技术门槛不高,但真正扛住午高峰的并发,才知道坑在哪。我们的项目日均 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% 的性能瓶颈,是性价比最高的第一步。如果你的小程序也在高峰期卡顿,建议先从这三个方向查起,别一上来就想着重构。

相关新闻

  • Sqribble深度解析:模板驱动的文档操作系统架构
  • 线性回归实战指南:从面试陷阱到工业级诊断与部署
  • 计算机毕业设计之“速餐”校园订餐系统的设计与实现

最新新闻

  • QtAdb:让Android调试从命令行到图形化的革命
  • FanControl深度解析:如何通过智能风扇控制提升电脑性能与静音体验
  • RFID技术助力高端精密设备流向追溯管理
  • LeetDown终极指南:macOS平台iOS设备降级实战手册
  • 假新闻检测实战:轻量模型+特征工程+智能调参工作流
  • Twitter如何提高曝光率?twitter流量分析

日新闻

  • 利用微PE工具箱进行系统安装教程
  • 渗透测试十大核心工具实战指南:从信息搜集到报告生成全流程解析
  • 暗黑破坏神2存档编辑器:网页版角色修改工具完全指南

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号