食堂点餐系统的并发模型非常极端:全天 24 小时里,80% 的流量集中在 11:30~12:30 这六十分钟;开餐瞬间,几千名学生几乎同时点"今日爆款"。我们为高校食堂点餐系统设计架构时,把核心挑战归纳为一句话:读多写多、热点集中、不允许超卖、不允许丢单。本文拆解四个关键设计。

一、先把流量画出来:高峰曲线决定架构

按一所 1.5 万人的学校估算:

时段请求特征峰值 QPS(估算)
10:00~11:00浏览菜单、预约点单50~100
11:20~11:30刷新菜品、占座300~500
11:30~12:00下单、支付、取餐号1500~3000
14:00 以后长尾查询订单< 20

结论:不要按峰值配机器,按"削峰 + 扛住 5 分钟风暴"设计。峰值 QPS 3000 中,约 85% 是读(菜单/排队进度),10% 是下单,5% 是支付回调——读的优化空间最大。

二、读路径:多级缓存 + 静态化

1. 菜单静态化

当日菜单在凌晨由定时任务生成 JSON,推到 CDN/对象存储,客户端直读 CDN,根本不经过应用服务器:

// 凌晨 3:00 定时任务(伪代码)
const menu = await buildDailyMenu();     // 聚合菜品、库存快照、价格
await oss.put(`menu/${date}.json`, JSON.stringify(menu));
await cdn.prewarm(`https://cdn.xxx.com/menu/${date}.json`);

库存变化频繁的部分不放进静态 JSON,由接口单独提供,只缓存 3 秒——用户感知几乎实时,数据库压力降一个数量级。

2. 满屏的"已售罄"标签

售罄状态用 Redis 的 BitMap 或 Set 存售罄菜品 ID,接口读内存级数据,单机轻松几万 QPS。

三、写路径:库存扣减的三种姿势

超卖是点餐系统第一大事故。三种方案按可靠性递增:

方案 A:数据库乐观锁(小规模可用)

UPDATE dishes SET stock = stock - 1
WHERE id = 1024 AND stock > 0;
-- 受影响行数为 0 即售罄,回滚事务

简单可靠,但高峰期几百个事务排队在行锁上,数据库 CPU 先顶不住。

方案 B:Redis 预扣(推荐)

库存预热进 Redis,用 Lua 保证"检查 + 扣减"原子性,扣成功才落库:

-- decr_stock.lua
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
  redis.call('DECR', KEYS[1])
  return 1
end
return 0
const ok = await redis.eval(lua, 1, `stock:${dishId}`);
if (!ok) throw new BizError('该菜品已售罄');
// 异步落库 + 生成订单记录
await mq.publish('order.create', { dishId, userId });

方案 C:令牌桶限量 + 排队(超大规模)

开餐瞬间先发"库存令牌",抢到令牌才能进下单流程,未抢到的直接显示"排队第 N 位",体验可控。

四、幂等:防止重复下单与重复扣款

弱网环境下用户连点三次"提交订单",或支付回调重试三次——没有幂等就是资损事故。

  • 客户端:进入下单页时向后端申请一次性 orderToken,提交携带,服务端用 SETNX token:xxx 判重,第二次直接拒绝
  • 支付回调:微信/支付宝会重试通知,回调处理必须先查订单状态,已支付直接返回 success,业务动作放幂等表里去重
app.post('/api/pay/notify', async (req, res) => {
  const ok = verifySignature(req.body);          // 1. 验签
  const first = await redis.set(`pay:${req.body.outTradeNo}`, 1, 'NX', 'EX', 600);
  if (!first) return res.send('success');        // 2. 幂等:重复通知直接确认
  await markOrderPaid(req.body.outTradeNo);      // 3. 业务处理
  res.send('success');
});

五、削峰:消息队列的两种用法

  1. 异步化非关键路径:下单成功后的小票打印、取餐号推送、积分发放全部丢进队列,下单接口只做"扣库存 + 建订单"两件事,P99 从 800ms 降到 120ms
  2. 落库缓冲:订单写 MySQL 前先进 Redis Stream / RabbitMQ,消费端按 200 TPS 稳速入库,数据库永远不抖
经验值:点餐高峰的消息队列积压在 1 万条以内都能被 12:30 前消化完,用户无感知。宁可排队 3 秒,不要数据库雪崩。

六、兜底预案

  • 降级开关:高峰期关闭"菜品详情评论"等非核心功能,配置中心一键下发
  • 本地缓存兜底:Redis 不可用时,应用内存缓存上一秒菜单数据,短暂牺牲实时性保可用
  • 压测验证:上线前用 JMeter 模拟"11:29:50 开始 3000 用户 10 秒内进入"场景,全链路压测至少跑三轮

小结

食堂点餐系统的架构没有黑科技,核心是把读做便宜、把写做原子、把意外做幂等、把峰值做平。这四件事做扎实,一台 4C8G 的机器撑住一所大学的午餐高峰并不夸张。