📖 本篇简介:美团系(本地生活/外卖/到店)面试的 MySQL 题围绕"交易履约"——订单状态机、库存扣减、优惠券、超时关单、对账、幂等、热点商家数据倾斜。本篇精选 20 道公开高频题型,覆盖状态机设计、防超卖、抢券防刷、本地消息表、超时任务、逆向退款、数据倾斜。题目均为公开面经常见类型,解析讲通用原理,不涉及任何公司未公开信息。前置知识:第 3、5、17、18、23 章;本系列示例基于 MySQL 8.0。
目录
- 1 本篇考情与知识地图
- 题目 1:订单状态机怎么设计
- 题目 2:库存扣减如何防超卖
- 题目 3:优惠券红包系统怎么设计
- 题目 4:高并发抢券如何防刷
- 题目 5:商户商品多条件查询表怎么设计
- 题目 6:超时未支付订单怎么自动取消
- 题目 7:外卖订单状态流转如何保证幂等
- 题目 8:对账系统怎么设计
- 题目 9:分库分表按用户还是按商家
- 题目 10:本地消息表如何保证最终一致
- 题目 11:秒杀库存如何预热
- 题目 12:动态多条件查询索引怎么建
- 题目 13:退款逆向流程数据怎么设计
- 题目 14:大促前DB压测怎么做
- 题目 15:读多写少怎么用缓存
- 题目 16:MQ和DB双写一致性
- 题目 17:订单表归档策略
- 题目 18:重复下单如何幂等
- 题目 19:热点商家数据倾斜怎么办
- 题目 20:O2O场景MySQL常见瓶颈点
- 速查速记
- 常见坑与注意事项
- 复习清单
- 练习题
1 本篇考情与知识地图
美团系面试围绕"一笔外卖订单从下单到送达再到退款"展开,核心是状态机 + 幂等 + 最终一致。先建地图:
| 环节 | 数据特点 | 关键技术 |
|---|---|---|
| 下单 | 状态多、并发高 | 幂等 + 原子扣减 |
| 支付 | 状态机、回调 | 状态单向流转 |
| 履约 | 长时间状态 | 超时任务 |
| 退款 | 逆向 | 逆向流水 |
| 对账 | 钱要对平 | T+1 对账 |
主线:每一次状态流转都要幂等、可追踪、可对账。
题目 1:订单状态机怎么设计
考点
- 状态枚举;单向流转;防乱序。
参考答案
1. 状态枚举清晰:待支付、已支付、履约中、已完成、已取消、退款中、已退款;
2. 单向流转:状态只能按约定路径前进,不能跳变;
3. 条件更新:UPDATE order SET status=新 WHERE id=? AND status=旧,受影响 0 行说明状态已变;
4. 状态流水:每次流转记一条状态变更日志,便于追踪;
5. 并发安全:靠 where 旧状态条件保证并发不会乱序覆盖。
-- 状态机条件更新,本示例基于 MySQL 8.0
UPDATE t_order SET status = 2, pay_time = NOW()
WHERE id = 88001 AND status = 1; -- 仅当仍是待支付(1)时才置为已支付(2)
追问
- 问:重复回调怎么办?答:where status=1,已支付后再回调影响 0 行,天然幂等。
- 问:要回退状态(如支付失败)?答:状态机允许明确的逆向边,但要记流水。
题目 2:库存扣减如何防超卖
考点
- 原子更新;条件扣减。
参考答案
1. 原子 SQL:UPDATE stock SET count=count-1 WHERE sku=? AND count>0;
2. 别先查后扣:并发间隙会超卖;
3. 预占库存:下单先冻结库存,支付确认再真正扣;
4. 超时释放:未支付超时自动解冻回补;
5. 对账:库存与订单对账兜底。
追问
- 问:预占和实扣区别?答:预占防超卖+支持超时释放,实扣是最终扣减。
- 问:高并发预占?答:热点库存前置 Redis,异步落 DB。
题目 3:优惠券红包系统怎么设计
考点
- 券模板 + 用户券;库存。
参考答案
1. 券模板表:定义券类型、面额、库存、有效期;
2. 用户券表:谁领了哪张券、状态(未用/已用/过期);
3. 领券原子扣库存:模板库存 WHERE count>0 原子扣;
4. 用券幂等:一张券只能用一次,WHERE status=未用;
5. 过期处理:定时任务把过期券置为已过期。
-- 领券:扣模板库存 + 写用户券,本示例基于 MySQL 8.0
START TRANSACTION;
UPDATE t_coupon_tpl SET remain = remain - 1 WHERE tpl_id = 5 AND remain > 0;
INSERT INTO t_user_coupon(user_id, tpl_id, status, created_at)
VALUES (1001, 5, 0, NOW()); -- 0=未使用
COMMIT;
追问
- 问:一人限领一张怎么保证?答:
UNIQUE(user_id, tpl_id),重复领冲突报错。 - 问:券超发了?答:模板 remain 原子扣 + 对账,兜底。
题目 4:高并发抢券如何防刷
考点
- 限流;去重;前置拦截。
参考答案
1. 限领规则:一人一张,唯一索引兜底;
2. 限流:按用户/IP 限流,防脚本刷;
3. 验证码/风控:领券前过人机校验;
4. 库存前置 Redis:先扣 Redis,异步发券落库;
5. 黑名单:识别薅羊毛批量拦截。
追问
- 问:Redis 扣了但 DB 发券失败?答:异步补偿 + 对账,失败回补 Redis 库存。
- 问:库存怎么同步到 Redis?答:活动开始前预热全量,扣减后异步持久化。
题目 5:商户商品多条件查询表怎么设计
考点
- 组合索引;ES 兜底。
参考答案
1. 核心条件走索引:商户 ID、分类、状态等高频过滤列建联合索引;
2. 动态条件:条件组合多,单靠联合索引覆盖不全;
3. ES 兜底:复杂多条件/排序/距离检索走 ES;
4. 冗余字段:高频展示字段冗余进商品表;
5. 避免全表扫:任何查询必须带商户/区域等强过滤条件。
追问
- 问:多个可选条件怎么建索引?答:把固定条件建索引,动态条件交给 ES。
- 问:距离排序?答:地理空间索引或 ES geo,别在 DB 里算距离。
题目 6:超时未支付订单怎么自动取消
考点
- 延迟任务;状态机。
参考答案
1. 定时任务扫单:扫"待支付且超时"订单,置为取消并释放库存;
2. 延迟队列:下单时发延迟消息,到点检查是否仍未支付,更实时;
3. 条件取消:UPDATE order SET status=取消 WHERE id=? AND status=待支付 AND create_time<超时;
4. 幂等:已支付的单取消不生效(where 状态拦截);
5. 库存释放:取消同时回补预占库存。
-- 超时关单:仅取消仍处于待支付的单,本示例基于 MySQL 8.0
UPDATE t_order
SET status = 5, cancel_time = NOW()
WHERE status = 1 AND created_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE);
追问
- 问:扫单会不会扫到刚支付的?答:where status=1,已支付不受影响;且到点再查支付状态。
- 问:延迟队列比定时扫好在哪?答:实时、不扫全表;但要可靠投递。
题目 7:外卖订单状态流转如何保证幂等
考点
- 状态机幂等;重复消息。
参考答案
1. 条件更新:每次流转 WHERE status=旧,重复流转影响 0 行;
2. 事件去重:MQ 消费按事件 ID 去重;
3. 状态流水:每次流转记流水,可重放入口;
4. 单向:已完成状态不允许回退;
5. 对账:状态与支付/履约系统对账。
追问
- 问:骑手状态和订单状态不一致?答:以履约系统事件为准,驱动订单状态流转。
- 问:重复推送事件?答:事件 ID 去重表拦截。
题目 8:对账系统怎么设计
考点
- 多方对账;差异处理。
参考答案
1. 三方对账:订单、支付、商户账单逐笔核对;
2. T+1 全量:夜间跑昨日数据比对;
3. 差异清单:自动输出金额/状态不符的单;
4. 自动补偿:可自动修复的自动补,复杂的进工单;
5. 准实时:资金类关键链路近实时对账。
追问
- 问:对账跑在线库?答:迁到从库/离线库,别压在线。
- 问:长短款?答:记差异账,财务介入处理。
题目 9:分库分表按用户还是按商家
考点
- 查询模式决定分片键。
参考答案
1. 用户高频看自己订单:按 user_id 分片最自然;
2. 商家高频看自己接单:按 merchant_id 分片;
3. 两难:两个维度都高频——主分片按一个,另一维度建路由表/同步 ES;
4. 权衡:C 端用户量大、QPS 高,通常按 user_id 分片;
5. 运营检索:多维分析全走 ES/数仓。
追问
- 问:选 user_id 后商家怎么查?答:merchant_order 路由索引表,或 ES。
- 问:大商家订单量巨大?答:本身就是热点,单独处理。
题目 10:本地消息表如何保证最终一致
考点
- 同事务写消息;异步投递。
参考答案
1. 业务表与消息表同事务:要么都成功;
2. 后台扫表投递 MQ:成功标记,失败重试;
3. 消费幂等:按消息 ID 去重;
4. 死信 + 人工:超限进死信;
5. 清理:已发消息定期删。
追问
- 问:消息表和业务表必须同库?答:是,本地事务才有效;跨库用事务消息。
- 问:投递重复?答:消费端幂等兜底。
题目 11:秒杀库存如何预热
考点
- Redis 预热;异步落库。
参考答案
1. 活动前把库存加载进 Redis;
2. 用户请求先在 Redis 原子扣(Lua 保证原子);
3. 下单成功异步扣 DB;
4. Redis 与 DB 对账:异步核对差异;
5. 活动结束落盘:最终库存以 DB 为准。
追问
- 问:Redis 扣减怎么原子?答:Lua 脚本把"判断+扣减"放一个脚本里执行。
- 问:Redis 挂了?答:降级到 DB 限流扣减,或直接停止活动保 DB。
题目 12:动态多条件查询索引怎么建
考点
- 组合索引覆盖;ES。
参考答案
1. 固定条件建联合索引:如 (merchant_id, status);
2. 可选条件多:不可能每个组合都建索引;
3. 高频组合建专属索引,低频走 ES;
4. 避免索引过多:写放大;
5. 强制带过滤:查询必须带商户/区域强过滤列。
追问
- 问:可选条件拼 SQL?答:动态 SQL 拼装,配合索引;拿不准就 ES。
- 问:索引建多了?答:单表索引建议 5 个以内。
题目 13:退款逆向流程数据怎么设计
考点
- 逆向流水;状态机。
参考答案
1. 退款单表:关联原订单,记录退款金额、原因、状态;
2. 逆向流水:每笔退款记一条资金流水;
3. 状态机:退款申请 → 退款中 → 退款成功/失败;
4. 幂等:同一退款单重复处理不重复退款;
5. 对账:退款与支付对账。
追问
- 问:部分退款?答:退款单支持多次,累计不超过原单金额。
- 问:库存要回退吗?答:退款成功回补对应库存。
题目 14:大促前DB压测怎么做
考点
- 压测方法;容量。
参考答案
1. 流量回放:用历史真实流量压测,混合读写;
2. 压测目标:找到单库 TPS/QPS 上限与拐点;
3. 容量规划:按预估峰值 × 安全系数配资源;
4. 预热:热点数据进 Buffer Pool/缓存;
5. 预案演练:限流、降级、切换演练;
6. 压测后调优:根据瓶颈加索引/分库/缓存。
追问
- 问:怎么压才真实?答:别纯 select,按线上读写比例混合,带真实数据量。
- 问:压测发现 CPU 打满?答:先优化慢 SQL/索引,再考虑扩容。
题目 15:读多写少怎么用缓存
考点
- 缓存策略;一致性。
参考答案
1. 热点读挡 Redis:商户信息、商品详情等读多写少;
2. Cache-Aside:读 miss 查库回填,写后删缓存;
3. 写少:可接受缓存 TTL 兜底;
4. 防穿透:空值缓存/布隆过滤;
5. 防击穿:热点 key 互斥重建;防雪崩:TTL 加随机。
追问
- 问:写频繁的对象也缓存?答:写频繁缓存失效频繁,权衡命中率。
- 问:缓存和 DB 不一致?答:延迟双删 + TTL。
题目 16:MQ和DB双写一致性
考点
- 本地消息表;可靠投递。
参考答案
1. 先写库再发消息:用本地消息表绑事务;
2. 可靠投递:消息落库后异步投递,不丢;
3. 消费幂等:重复消费无副作用;
4. 失败重试 + 死信;
5. 对账:MQ 消息与业务状态对账。
追问
- 问:先发消息再写库?答:可能消息发了库没写,消费方处理不存在的业务,不推荐。
- 问:事务消息?答:MQ 半消息也可,本地消息表更通用。
题目 17:订单表归档策略
考点
- 冷热分离;分区。
参考答案
1. 在线只留近 N 月:更早迁归档库;
2. 按时间分区:DROP 老分区快速归档;
3. 归档校验后删:行数/checksum 一致再删在线;
4. 归档可查:提供只读入口;
5. 备份兜底。
追问
- 问:归档后历史订单查不到?答:归档库提供查询入口,业务按时间路由。
- 问:多久归档一次?答:按月/按季,在线库控制在千万级。
题目 18:重复下单如何幂等
考点
- 幂等键;唯一约束。
参考答案
1. 前端/客户端带幂等键:如 token,每次下单唯一;
2. 唯一索引:(user_id, 业务幂等键) 唯一约束;
3. 重复请求拦截:重复下单直接返回已有订单;
4. 支付幂等:支付回调按支付单号去重;
5. 状态机兜底:状态已变则不重复处理。
追问
- 问:用户连点两下?答:幂等键 + 唯一索引,第二次冲突返回同一单。
- 问:幂等键谁生成?答:前端进下单页时申请一个一次性 token。
题目 19:热点商家数据倾斜怎么办
考点
- 分片倾斜;热点处理。
参考答案
1. 分片键别让大商家集中:按 user_id 分片时某大商家订单分散到多片是好事;
2. 单独热点处理:超大商家订单单独路由/扩容;
3. 读热点:商家接单界面挡 Redis;
4. 写热点:接单更新合并异步化;
5. 监控倾斜:分片 QPS 不均及时发现。
追问
- 问:按商家分片遇到大商家?答:单个商家占满一片,要拆分或单独路由。
- 问:读热点商家?答:商户信息缓存,接单列表分页走索引。
题目 20:O2O场景MySQL常见瓶颈点
考点
- 瓶颈识别。
参考答案
1. 下单写并发:扣库存 + 建订单,热点商品行锁竞争;
2. 订单列表深分页:用户翻历史订单慢;
3. 多条件检索:商户/位置/分类组合查询索引难覆盖;
4. 状态流转频繁:履约状态更新多;
5. 大表归档:订单表持续膨胀;
6. 对应手段:扣减前置、游标分页、ES 检索、状态机条件更新、分区归档。
追问
- 问:最容易先扛不住的?答:热点商品扣库存和订单列表查询。
- 问:怎么提前发现?答:压测 + 大促大盘监控。
速查速记
| 主题 | 核心手法 | 关键 SQL/动作 |
|---|---|---|
| 状态机 | 条件更新 | WHERE status=旧 |
| 防超卖 | 原子扣减 | WHERE count>0 |
| 超时关单 | 延迟队列/扫单 | 条件取消+释放库存 |
| 抢券 | Redis前置+限流 | 一人一单唯一索引 |
| 幂等 | 业务唯一键 | 唯一约束去重 |
| 对账 | T+1三方核对 | 差异自动补偿 |
| 多条件 | ES兜底 | 固定条件建索引 |
常见坑与注意事项
- 坑 1:先查库存再扣——并发超卖,必须一条 SQL 原子完成;
- 坑 2:状态流转无条件更新——重复回调导致状态错乱,必须带旧状态条件;
- 坑 3:超时关单扫全表——无索引扫超时订单拖库,按时间+状态索引分批;
- 坑 4:退款不幂等——重复退款造成资金损失,退款单唯一约束;
- 坑 5:分片键只考虑一个查询维度——上线才发现另一维度查不动;
- 注意:本篇均为公开题型与通用交易系统设计,不代表任何公司真实方案。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| 状态机 | 单向+条件更新 | 别无条件改状态 |
| 防超卖 | 原子扣减 | 别先查后扣 |
| 优惠券 | 模板+用户券 | 一人一单唯一索引 |
| 超时关单 | 延迟队列 | 已支付别误关 |
| 幂等 | 业务唯一键 | 支付回调要去重 |
| 分片键 | 查询模式决定 | 别只看一个维度 |
| 对账 | 三方T+1 | 别压在线库 |
| 数据倾斜 | 热点单独处理 | 监控分片均衡 |
练习题
练 1:写一条"待支付 30 分钟未支付自动取消并保证不误伤已支付订单"的 SQL。
<details>
<summary>参考答案</summary>
UPDATE t_order
SET status = 5, cancel_time = NOW()
WHERE status = 1
AND created_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE);
status=1(待支付)作为条件,已支付(status=2)的订单不受影响;再配合库存回补和库存预占释放,完成超时关单。
</details>
练 2:用户下单时连点两次产生两笔订单,从 DB 层怎么杜绝?
<details>
<summary>参考答案</summary>
前端进入下单页申请一次性幂等 token,下单时把 token 随请求带上;订单表对 (user_id, idempotent_token) 建唯一索引,第二次提交触发唯一冲突,应用捕获后返回已有订单而非新建;支付回调同样按支付单号幂等。
</details>
下一篇预告:第 33 章 京东系MySQL真题精讲
文章回复
0 条公开回复