食堂点餐系统的并发模型非常极端:全天 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');
});
五、削峰:消息队列的两种用法
- 异步化非关键路径:下单成功后的小票打印、取餐号推送、积分发放全部丢进队列,下单接口只做"扣库存 + 建订单"两件事,P99 从 800ms 降到 120ms
- 落库缓冲:订单写 MySQL 前先进 Redis Stream / RabbitMQ,消费端按 200 TPS 稳速入库,数据库永远不抖
经验值:点餐高峰的消息队列积压在 1 万条以内都能被 12:30 前消化完,用户无感知。宁可排队 3 秒,不要数据库雪崩。
六、兜底预案
- 降级开关:高峰期关闭"菜品详情评论"等非核心功能,配置中心一键下发
- 本地缓存兜底:Redis 不可用时,应用内存缓存上一秒菜单数据,短暂牺牲实时性保可用
- 压测验证:上线前用 JMeter 模拟"11:29:50 开始 3000 用户 10 秒内进入"场景,全链路压测至少跑三轮
小结
食堂点餐系统的架构没有黑科技,核心是把读做便宜、把写做原子、把意外做幂等、把峰值做平。这四件事做扎实,一台 4C8G 的机器撑住一所大学的午餐高峰并不夸张。