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

发布时间:2026/6/25 21:16:15
餐饮外卖点餐小程序源码性能优化实录(附代码)——Redis 热点缓存、接口限流与数据库索引设计 餐饮外卖小程序看起来技术门槛不高但真正扛住午高峰的并发才知道坑在哪。我们的项目日均 8000 单高峰期 90 分钟内涌入 5000 请求上线第一周就被打崩菜品接口响应近 5 秒下单超时率 12%数据库 CPU 长期顶在 95%。复盘下来问题就三个热点数据全打数据库、接口没有限流保护、SQL 查询压根没建索引。这篇文章不讲理论只记录我们怎么一个一个解决这些问题。方案全部在生产环境跑过核心代码直接贴出来能复用的你直接拿走。源码与演示c.ymzan.topRedis 热点缓存解决所有人都在查同一批菜1. 问题分析点餐小程序最典型的场景是几百人同时打开首页查看同一家店的菜品列表。此时数据库会收到大量重复查询SELECT*FROMdishWHEREshop_id101ANDstatus1ORDERBYsort_desc;这条 SQL 在高峰期每秒被执行 200 次但数据本身 5 分钟内不会变。2.方案本地缓存 Redis 二级缓存我们采用了Caffeine 本地缓存L1 RedisL2的两级架构L1单实例缓存命中后无需任何网络开销适合极端热点L2分布式缓存保证多实例间数据一致关键代码ServicepublicclassDishService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;// Caffeine 本地缓存最大 500 条过期时间 60 秒privatefinalCacheLong,ListDishVOlocalCacheCaffeine.newBuilder().maximumSize(500).expireAfterWrite(60,TimeUnit.SECONDS).build();privatestaticfinalStringDISH_CACHE_KEYdish:shop:;privatestaticfinallongCACHE_TTL300;// Redis 缓存 5 分钟publicListDishVOgetDishList(LongshopId){// L1本地缓存ListDishVOcachedlocalCache.getIfPresent(shopId);if(cached!null){returncached;}// L2Redis 缓存StringkeyDISH_CACHE_KEYshopId;cached(ListDishVO)redisTemplate.opsForValue().get(key);if(cached!null){localCache.put(shopId,cached);// 回填 L1returncached;}// 缓存未命中查数据库cacheddishMapper.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_KEYdish.getShopId());localCache.invalidate(dish.getShopId());}}3. 热点 key 的额外处理午餐高峰时头部商家日均 500 单的菜品查询会成为热点 key所有请求都打到同一个 Redis 节点。我们的解法是key 加随机后缀分散访问。在写入缓存时// 写入时加随机后缀StringkeyDISH_CACHE_KEYshopId:ThreadLocalRandom.current().nextInt(10);// 读取时尝试 10 个后缀for(inti0;i10;i){ObjectvalredisTemplate.opsForValue().get(DISH_CACHE_KEYshopId:i);if(val!null)return(ListDishVO)val;}这让原本集中在一个节点的请求分散到了 10 个 key 上Redis 节点 CPU 从 80% 降到了 15%。优化效果菜品列表接口 P99 从 4.8s 降到 120ms。接口限流防止恶意请求和突发流量击穿系统1. 问题分析除了正常高峰流量我们还遇到过两类问题前端重复提交用户网络卡顿时疯狂点击下单同一订单被提交 5-8 次恶意刷接口有人用脚本高频调用菜品查询接口挤占正常用户资源2. 方案Guava RateLimiter Redis 分布式限流单机限流用Guava RateLimiter分布式场景用Redis Lua 脚本。单机限流下单接口ComponentpublicclassOrderRateLimiter{// 每秒只允许 100 次下单privatefinalRateLimiterrateLimiterRateLimiter.create(100.0);publicbooleantryAcquire(){returnrateLimiter.tryAcquire(50,TimeUnit.MILLISECONDS);}}// 在 Controller 中使用PostMapping(/order/create)publicResultcreateOrder(RequestBodyOrderDTOdto){if(!orderRateLimiter.tryAcquire()){returnResult.fail(系统繁忙请稍后重试);}// 正常下单逻辑...}分布式限流菜品查询接口Redis LuapublicbooleantryAcquire(StringuserId,StringapiPath,intmaxRequests,intwindowSeconds){Stringkeyrate_limit:apiPath:userId;StringluaScriptlocal current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return tonumber(current) tonumber(ARGV[2]);LongresultredisTemplate.execute(newDefaultRedisScript(luaScript,Long.class),Collections.singletonList(key),windowSeconds,maxRequests);returnresult!nullresult1L;}限流规则我们设为接口限流阈值窗口菜品查询30 次/用户/分钟滑动窗口下单创建5 次/用户/分钟固定窗口商户后台查询100 次/IP/秒滑动窗口优化效果重复下单率从 8% 降到 0.3%恶意刷接口被完全拦截。数据库索引设计让慢查询消失1. 问题分析优化前我们用EXPLAIN分析了下单高峰期最慢的三条 SQLSQL扫描行数执行时间查询用户近 30 天订单86,0002.1s查询商家今日销量120,0003.4s查询骑手待取餐订单45,0001.8s全表扫描没有任何有效索引。2. 索引优化方案原则索引不是越多越好只建高频查询需要的联合索引。① 订单查询优化用户侧 商户侧-- 用户查询自己的订单user_id create_time 联合索引CREATEINDEXidx_order_user_timeONorder(user_id,create_timeDESC);-- 商户查询今日订单shop_id create_time 联合索引CREATEINDEXidx_order_shop_timeONorder(shop_id,create_timeDESC);-- 骑手查询待取餐status assign_time 联合索引CREATEINDEXidx_order_status_timeONorder(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_timeCURDATE()ANDcreate_timeCURDATE()INTERVAL1DAY优化效果三条慢查询全部降到 50ms 以内数据库 CPU 从 95% 降到 30%。结语回过头看这次优化本质上就做了三件事用两级缓存扛住热点读用双层限流挡住异常流量用精准索引消灭慢查询。没有引入复杂的中间件没有重构架构但效果立竿见影——P99 从 4.8 秒降到 120 毫秒数据库 CPU 从 95% 回落到 30%。当然优化没有终点。后来我们又陆续处理了缓存击穿、索引失效、连接池配置等问题但这三板斧解决了 80% 的性能瓶颈是性价比最高的第一步。如果你的小程序也在高峰期卡顿建议先从这三个方向查起别一上来就想着重构。