面试复习资料 列表全部文章
面试复习资料

面试复习:系统设计与场景题(秒杀、短链、排行榜、红包、扫码登录、TopK、大文件上传、订单关闭)初中高级

系统设计题没有标准答案,但有标准套路。整理回答框架与 10 道高频场景题(秒杀、消息中台、短链、排行榜、红包、扫码登录、TopK 去重、大文件上传、订单超时关闭),每题含架构图要点、容量估算和取舍,分初中高三层。

更多

系统设计题是我见过最容易"会的人讲不清、不会的人讲太满"的题型。面试官说一句"设计一个秒杀系统",有人张嘴就是 Redis + MQ + 限流,讲了十分钟,面试官问一句"你这个系统 QPS 多少?库存放哪?超卖怎么保证?",就卡住了。我总结下来,系统设计不是背架构图,而是一套追问自己的流程:需求 → 估算 → 主流程 → 瓶颈 → 取舍 → 兜底。

这篇文章按 🌱/🌿/🌳 三层组织:

  • 🌱 初级(0–3 年):能说清楚"核心流程和用到的组件",知道基本方案;
  • 🌿 中级(3–5 年):能说清楚"为什么这么选、瓶颈在哪、常见坑是什么";
  • 🌳 高级(5 年+,对标后端约 8 年):能做容量估算、讲取舍与降级、讲故障场景和数据对账、讲演进路径。

每道题给出:架构要点 + 容量估算 + 取舍,并附追问链。技术栈不局限,参考的是公开面经里反复出现的题目,具体数据均为"估算示例",面试时要用你自己业务的数字。

一、知识地图与回答框架

先给一个我自己一直用的"六步框架"。无论题目是什么,按这个顺序答,至少不会乱:

① 澄清需求功能边界读写比/一致性 ② 容量估算QPS/存储/带宽峰值系数 x3~10 ③ 主流程API + 数据模型先画一条路径 ④ 找瓶颈热点/单点/慢点逐个击破 ⑤ 取舍一致性 vs 性能成本 vs 可用 ⑥ 兜底与演进降级/限流/对账监控告警/灰度 估算示例:日活 1000 万,人均 5 次请求 → 日请求 5000 万;均值 ≈ 5000万 / 86400 ≈ 580 QPS; 峰值按 5–10 倍算 ≈ 3000–6000 QPS。大多数业务单库扛不住写,但扛得住读缓存后的回源。 面试要点:数字不需要精确,需要"数量级 + 推理过程"

🌱 初级:回答系统设计题的基本套路

标准回答要点:

  • 先复述并澄清:"我理解的需求是……,用户规模、读写比例、是否要强一致,我先做这几个假设,可以吗?"——这一步既争取思考时间,又体现工程习惯;
  • 先给最小可用方案(MVP):单体 + 一个数据库怎么做,再逐步说明"当流量涨 100 倍会出什么问题、怎么加缓存/队列/分片";
  • 用到的组件要说明为什么:为什么用 Redis(读多写少、低延迟)、为什么用 MQ(削峰、解耦、异步)、为什么分库分表(单表超千万行/单库写入瓶颈)。

追问链:为什么不直接上微服务?——小规模没必要,先保证主流程正确,演进路径清晰即可。

🌿 中级:容量估算方法论

估算不是玄学,就是几个固定公式:

指标公式例子
平均 QPS日请求数 / 86400(约 10 万秒)1 亿 / 10 万 ≈ 1000
峰值 QPS平均 QPS × 峰值系数(一般 3–10,秒杀/活动可到 100+)1000 × 5 = 5000
存储单条大小 × 日增条数 × 保留天数 × 副本数1KB × 1 亿 × 365 × 3 ≈ 110TB
带宽QPS × 单响应大小 × 85000 × 10KB × 8 = 400Mbps
单机能力(经验值)Redis 单实例读写约 5 万–10 万 QPS;MySQL 单机热点写约千级 TPS;普通业务服务单机数百到数千 QPS决定机器数量与分片数

这些单机数字是经验量级,实际取决于硬件、命令复杂度、网络、SQL 复杂度,面试时说"按压测为准,这里取保守值"更稳。

🌳 高级:把设计题讲成"决策记录"

高级岗位的面试官其实在看你怎么做决策。我的建议是每个关键点都用"方案 A/B → 选择 → 代价 → 何时重新评估"的句式,比如"库存放 Redis 还是 DB:Redis 快但持久化有丢失窗口,所以 Redis 做预扣、DB 做最终一致落账,并有对账兜底;如果 QPS 下降到 DB 可承受,可以简化去掉 Redis 预扣"。这种讲法体现的是可演进性而不是"炫组件"。

二、秒杀系统

面试出现频率第一的场景题。核心矛盾:极短时间内的巨量请求 vs 极少的库存,绝大部分请求注定失败,所以设计目标是"把无效请求挡在越靠前的地方越好"。

CDN / 静态化页面、商品图 网关限流/验证码/黑名单 秒杀服务校验资格/一人一单 Redis 预扣库存Lua 原子:判重+扣减 MQ 削峰成功者异步下单 订单服务幂等消费/落库 MySQL条件更新扣真实库存 漏斗:100 万请求 → CDN/网关挡掉 90%+ → Redis 只放行库存数量 → DB 只处理成功订单 Lua 脚本示例:if stock>0 and 未购买 then stock-- 并记录用户 else 失败

🌱 初级:核心思路

  1. 页面静态化 + CDN:商品详情、图片、JS 全部走 CDN,只有"下单"请求打到后端;
  2. 按钮防连点 + 前端限流:倒计时到点才放开按钮,请求带时间校验;
  3. Redis 预扣库存:活动开始前把库存预热进 Redis,抢购时用 DECR/Lua 扣减,返回负数即售罄;
  4. MQ 异步下单:Redis 扣成功的请求进队列,订单服务慢慢消费,保护数据库;
  5. 一人一单:Lua 中同时用 Set 判重(SISMEMBER + SADD)或订单表唯一键(用户 + 活动)兜底。
-- 秒杀 Lua:判重 + 扣库存 一个原子步骤
-- KEYS[1]=库存key  KEYS[2]=已购用户set  ARGV[1]=userId
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return 2 end   -- 已购买
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then return 1 end                       -- 售罄
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 0                                                               -- 抢购成功

🌿 中级:超卖、少卖、重复下单怎么防

  • 超卖:Redis 端靠 Lua 原子扣减;数据库端最终落库用条件更新 update stock set num=num-1 where sku_id=? and num>=1,影响行数为 0 就失败,这是最后一道防线;
  • 少卖:Redis 扣了但订单创建失败(用户没付款/系统异常)→ 库存没回补。解决:订单超时未支付则关闭并回补 Redis 库存(回补也要幂等,记录"已回补"标记),失败进死信队列重试;
  • 重复下单:MQ 至少一次投递,消费者按"用户 + 活动"唯一键去重;
  • 热点 Key:单个商品库存 Key 全落在一个 Redis 分片上,成了热点。可以把库存拆成 N 份(stock:1001:0 ~ stock:1001:N),按用户 ID 哈希选一份扣,某份扣光再顺次尝试其他份。

🌳 高级:容量估算、降级与取舍

容量估算示例:某活动库存 1 万件,预计 50 万人同时点击,每人平均点 5 次,则峰值请求 250 万次,集中在开抢后 10 秒,即约 25 万 QPS。分层削减:CDN/静态化拦截页面类请求;网关按用户/设备限流(每人每秒 ≤1 次),假设可挡掉 70%,剩 7.5 万 QPS;Redis 单分片约 5 万–10 万 QPS,用 2–4 个分片承载;真正成功的只有 1 万单,MQ 消费者每秒处理 1000 单,10 秒内可处理完,数据库只承受约 1000 TPS,完全可控。结论:DB 从未直接面对峰值。

取舍与兜底:

  1. Redis 宕机怎么办:秒杀活动通常"可停不可错",可以暂停活动或切换到网关限流 + DB 条件更新的降级路径(吞吐低,但不会超卖);
  2. MQ 消息丢失:Redis 扣成功但消息没发出,用"本地消息表/事务消息",或活动结束后用 Redis 已购集合与订单表对账,补单或回补库存;
  3. 队列积压:秒杀成功后给用户展示"排队中",前端轮询结果(不阻塞连接);
  4. 防刷与黑产:验证码/答题削峰(同时拉长请求时间分布)、设备指纹、风控,秒杀的很多"流量"其实是脚本;
  5. 公平性:先到先得 vs 随机抽签,抽签模型可以把瞬时并发降为零(预约 + 开奖),是很多大促的实际做法。

追问链:库存扣减为什么不直接用分布式锁 + DB?——锁串行化会把吞吐限制在锁往返速度,峰值下大量线程排队超时,且锁本身可能成为故障点;原子脚本 + 预扣更轻。

三、短信/消息中台

核心是把"发送消息"从业务系统里抽出来变成公共能力:统一接入、模板管理、渠道路由、限流、幂等、重试、状态回执、成本统计。

🌱 初级:基本架构

业务方调用统一发送 API(携带业务方标识、模板编码、手机号、变量)→ 校验(签名、模板、黑名单、频控)→ 写入消息表并投递到 MQ → 发送服务消费,调用短信渠道(多家供应商)→ 回执回调更新状态。核心表:msg_task(任务)、msg_template(模板)、msg_channel(渠道配置)、msg_log(发送流水)。

🌿 中级:可靠性与治理

  • 幂等:业务方传 bizId,中台用 (appId, bizId) 唯一键,避免业务重试导致用户收到多条;
  • 频控:同一手机号 60 秒 1 条、1 小时 5 条、1 天 10 条(验证码类),用 Redis 计数或滑动窗口;
  • 多渠道路由与故障切换:按成功率/延迟/成本动态权重,某渠道失败率超过阈值自动熔断并切换备用渠道;
  • 重试:发送失败按退避重试,区分"可重试(超时)"和"不可重试(空号、黑名单)";
  • 优先级隔离:验证码(高优先、秒级)与营销短信(低优先、可延迟)使用不同的队列与线程池,避免营销群发把验证码挤住。

🌳 高级:合规、成本与观测

  1. 容量估算:营销群发 500 万条,要求 1 小时发完,则约 1400 条/秒;渠道方通常有并发限制,要在中台侧做"令牌桶按渠道限速",而不是无脑并发;
  2. 回执对账:发送成功≠送达。渠道回执可能晚到或丢失,需要定时主动查询状态,并做日对账(我方发送量、渠道计费量、回执量三方核对),差异用于结算与告警;
  3. 合规:营销类必须带退订、遵守发送时段,用户投诉/退订名单全局共享;
  4. 可观测:发送成功率、到达率、各渠道 P99、队列积压、各业务方用量,任意一项异常都应能告警;
  5. 演进:把"短信"抽象成"消息通道",扩展到站内信、邮件、App 推送、企业微信,共用模板、频控、路由、幂等能力。

四、短链接系统

🌱 初级:怎么把长链变短

核心是ID → 62 进制字符串。流程:接收长 URL → 生成唯一 ID(号段/雪花)→ 转成 base62(0-9a-zA-Z)→ 存 short_code → long_url 映射 → 访问时查映射并 302/301 重定向。6 位 base62 可表示 62^6 ≈ 568 亿个,7 位约 3.5 万亿,足够。

private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(long id) {
    StringBuilder sb = new StringBuilder();
    while (id > 0) { sb.append(CHARS.charAt((int) (id % 62))); id /= 62; }
    return sb.reverse().toString();
}

🌿 中级:细节与取舍

  • 301 vs 302:301 永久重定向会被浏览器缓存,之后不再经过你的服务,减轻压力,但你统计不到点击、也无法修改/下线目标;302 临时重定向每次经过服务,可统计与控制。需要数据分析的业务用 302;
  • 哈希法 vs 发号法:对长 URL 做 MurmurHash 取前几位,可能冲突,需要冲突检测与重试,且相同长链得到相同短链;发号法无冲突,但同一长链多次生成不同短码(可用长链哈希做去重表);
  • 短码不可预测:顺序 ID 转出的短码可被遍历。需要保密的场景,对 ID 做可逆混淆(异或、Feistel)或加随机位;
  • 缓存:读远多于写(约 100:1),热点短链放 Redis + 本地缓存(Caffeine),命中率 > 95% 后 DB 压力很小;对不存在的短码用布隆过滤器/空值缓存防穿透。

🌳 高级:容量与治理

容量估算:每天新增 1 亿条短链,保留 5 年约 1825 亿条(约需 8 位 base62);每条约 500 字节(长链 + 元数据)则约 90TB,需要分库分表(按短码哈希分片)+ 冷热分离(一年以上未访问的归档)。读 QPS:假设读写比 100:1,写 1200/s,读约 12 万/s,峰值再乘 3,缓存层是绝对主力。治理要点:过期与回收(设置有效期)、恶意链接检测与举报下线(钓鱼站点)、防刷(对同一 IP 创建频率限流)、点击统计走异步(写 MQ → 流计算 → OLAP),避免重定向链路被统计拖慢。

五、排行榜

🌱 初级:Redis ZSET

Redis 有序集合(Sorted Set)为排行榜而生:ZADD 添加/更新分数(O(log N))、ZINCRBY 增加分数、ZREVRANGE key 0 9 WITHSCORES 取前 10 名、ZREVRANK 查某人名次。同分时按成员字典序排序,如需"同分先到先得",可把时间戳编码进分数(例如 score * 2^20 + (MAX_TS - ts) 的整数组合,注意双精度 53 位尾数的精度上限)。

ZINCRBY rank:daily:20260929 10 user:1001
ZREVRANGE rank:daily:20260929 0 9 WITHSCORES
ZREVRANK rank:daily:20260929 user:1001

🌿 中级:多维度与大 key

  • 多周期榜:日榜、周榜、月榜各一个 key,带过期时间;周榜可由 7 个日榜用 ZUNIONSTORE 聚合;
  • 大 key 问题:千万级用户全放一个 ZSET,单 key 过大影响迁移与删除。方案:只保留 TopN(如前 1 万)在 Redis,其余用户名次用"分段 + 估算"(按分数段分桶统计人数)给出近似名次;
  • 数据来源:分数变化事件走 MQ 异步聚合后再写 ZSET,避免每个业务事件都直接写榜单;
  • 持久化:ZSET 是缓存视图,明细账(积分流水)在数据库,可重建榜单。

🌳 高级:亿级用户榜单与一致性

  1. 分片榜 + 合并:按用户哈希分为 N 个 ZSET,每个取 TopK,再在应用层归并得到全局 TopK(K 小时代价很低,全局名次用采样估算);
  2. 实时 vs 准实时:对"我的名次"要求实时的场景很少,大部分场景 5–30 秒刷新一次快照即可,把读压力打到只读快照缓存;
  3. 防作弊与回滚:刷分行为被判定后需要"扣分重算",所以要保留流水、支持按用户重算;
  4. 结算:榜单结算(发奖)必须以数据库/流水为准,使用某个时间点的快照,不能拿实时榜单直接发奖。

六、红包系统

🌱 初级:发红包与抢红包

流程:发红包时扣发送者余额、创建红包记录(总金额、个数、过期时间),并预先拆分金额放入 Redis List(或用二倍均值法实时计算);抢红包时从 List 中原子弹出一个金额,记录领取,并异步入账。二倍均值法:每次抢到的金额在 (0, 剩余金额/剩余个数 × 2) 之间随机,保证期望公平且最后一个人不会为 0(每人至少 0.01 元)。

// 二倍均值法:剩余 remain 分钱、left 个人
long max = remain / left * 2;
long amount = Math.max(1, ThreadLocalRandom.current().nextLong(1, max)); // 单位:分
if (left == 1) amount = remain;      // 最后一人拿走剩余

🌿 中级:关键难点

  • 并发抢与一人一次:Lua 脚本里"判断已领取 + 弹出金额 + 记录领取人"合成一个原子操作;
  • 资金安全:发红包扣款与红包创建必须在同一本地事务,或用事务消息;过期未领完的金额退回发送者,由延迟任务触发(幂等、可对账);
  • 入账异步化:抢到的金额先记录领取流水(状态"待入账"),MQ 异步增加用户余额,失败重试,入账幂等(红包 ID + 用户 ID 唯一键);
  • 热点:大群红包(几万人)会让一个 key 成为热点,可以按红包 ID 分片到不同 Redis 节点,把金额列表拆成多个子列表。

🌳 高级:账务对账与"钱守恒"

红包最核心的不是抢的速度,而是资金守恒:发出总额 = 已领取总额 + 已退回总额 + 未过期剩余。设计上要做到:每一步都有流水;流水表只追加、不修改;定时任务校验守恒公式,发现不平立即告警并冻结相关红包;退回操作以"红包状态机 + 唯一退款单"为准,避免重复退款。峰值应对上,春节类活动通常提前扩容、关闭非核心功能(降级),并做全链路压测与"资金类接口不降级、体验类接口先降级"的分级预案。

七、扫码登录

🌱 初级:流程

PC 网页展示二维码(内容是一个带唯一 qrId 的地址)→ 手机 App 扫码,请求服务器"确认登录该 qrId"(携带手机端登录态)→ 服务器把 qrId 状态改成"已确认"并生成 PC 登录凭证 → PC 端通过轮询/长轮询/WebSocket 得知并完成登录。

PC 浏览器 登录服务 手机 App ① 申请二维码 ② 返回 qrId(状态=待扫,TTL 2 分钟) ③ 轮询 qrId 状态 ④ 扫码(带 App 登录态)→ 状态=已扫描 ⑤ 点击"确认登录" → 状态=已确认 ⑥ 轮询得到一次性 ticket ⑦ ticket 换取会话(用后即焚) qrId 状态存 Redis:待扫 → 已扫描 → 已确认 / 已过期 / 已取消

🌿 中级:安全要点

  • 二维码一次性 + 短 TTL:qrId 随机不可预测,2 分钟过期,确认后立即失效;
  • "扫描"与"确认"分两步:扫码后手机端展示"某某设备请求登录,是否确认",防止用户被诱导扫码即登录(二维码钓鱼/"扫码即授权"攻击);
  • ticket 一次性:PC 拿到 ticket 后换会话,换取动作用 Lua 原子"读取并删除",防止被截获重放;
  • 绑定设备信息:把 PC 端的 IP、UA 在二维码生成时记录,确认页展示给用户核对;
  • 通知方式:轮询实现最简单(1–2 秒一次,注意限频);长轮询/WebSocket 体验更好、请求更少。

🌳 高级:规模化与风险

估算:日登录 500 万次,峰值 300 QPS 的二维码申请,但轮询会放大 50–100 倍(每个二维码存活 2 分钟、每 2 秒一次 → 60 次),峰值可达 2 万 QPS 的读,所以状态读走 Redis、避免落库;改用长连接推送可降低一个量级。风控方面:异地扫码风险提示、对异常频繁申请二维码的 IP 限流;跨端登录还要考虑"同账号多端互踢策略"和 token 刷新机制。

八、海量数据:TopK 与去重

🌱 初级:思路总览

海量数据题的核心是"内存放不下怎么办",答案基本是三个词:分治(哈希分片)、位图/布隆过滤器(省空间的去重)、堆(TopK)。

问题做法代价
10 亿个 QQ 号去重/判断是否存在位图 Bitmap:号码作下标,10 亿位 ≈ 125MB号码范围要可控
URL 黑名单判断(允许少量误判)布隆过滤器有误判率,不能删除(可用计数布隆)
统计 UV(允许约 1% 误差)HyperLogLog,Redis 最多约 12KB,标准误差 0.81%只能估算基数,不能取出元素
海量日志里访问最多的 Top100 IP哈希分片到多个小文件 → 各文件统计 → 小顶堆取 Top100 → 归并磁盘 IO
海量数据求中位数按值范围分桶计数,定位中位数所在桶后再细分多遍扫描

🌿 中级:分治 TopK 的完整步骤

题目:100GB 日志,每行一个 IP,内存 1GB,求出现次数最多的 100 个 IP。

  1. 哈希分片:hash(ip) % 1000 写入 1000 个小文件(约 100MB/个)。关键性质:相同 IP 一定落在同一文件,所以各文件的统计相互独立;
  2. 每个文件内统计:用 HashMap 计数(100MB 文件内存放得下);
  3. 每个文件取局部 Top100:用大小为 100 的小顶堆,遍历计数结果,比堆顶大就替换;
  4. 归并:1000 个文件 × 100 = 10 万条候选,再用堆选出全局 Top100。
PriorityQueue<Map.Entry<String, Integer>> heap =
    new PriorityQueue<>(Map.Entry.comparingByValue());   // 小顶堆
for (Map.Entry<String, Integer> e : counter.entrySet()) {
    if (heap.size() < K) heap.offer(e);
    else if (e.getValue() > heap.peek().getValue()) { heap.poll(); heap.offer(e); }
}

🌳 高级:在线场景与近似算法

  1. 流式 TopK(热搜、热点检测):用 Count-Min Sketch 估计频次 + 小顶堆维护 TopK,内存恒定;或滑动窗口 + 分片聚合再归并;
  2. 布隆过滤器参数:位数组大小 m、哈希函数个数 k、元素数 n,误判率约 (1 - e^(-kn/m))^k;1% 误判率约需要每元素 9.6 位、k≈7。缓存穿透防护场景,误判只是多查一次库,可接受;
  3. 实际工程选型:线上 UV 用 HyperLogLog(Redis PFADD/PFCOUNT/PFMERGE,按天分 key,周/月 UV 用 PFMERGE 合并);要求精确的(如结算)用离线 + 明细去重(Spark/Flink + 主键去重);
  4. 取舍:精确 vs 近似决定成本,先问业务"能容忍多大误差",能近似就近似。

九、大文件上传

🌱 初级:分片上传

把大文件切成固定大小的分片(如 5–10MB),逐个上传,最后合并。好处:失败只重传失败的分片、可并行上传提升吞吐、可暂停续传。对象存储(如 S3)官方就把它做成了"初始化 → 上传分片 → 完成合并"三步流程,官方建议 100MB 以上的对象使用分片上传;分片编号范围 1–10000。

🌿 中级:断点续传、秒传与校验

  • 文件指纹:前端计算文件 hash(MD5/SHA-256,大文件用 Web Worker 分块增量计算,避免卡 UI);
  • 秒传:上传前先带 hash 询问服务器,若已存在同 hash 的文件则直接返回,不再传输(注意校验防止伪造 hash:可要求补充文件头尾若干字节的校验,或服务端抽样校验);
  • 断点续传:服务器记录该上传任务(uploadId)已收到的分片编号,客户端重新进入时先查询已上传列表,只传缺失分片;
  • 分片校验与合并:每个分片带校验值;合并前检查分片数量与顺序;合并操作幂等;
  • 推荐架构:客户端直传对象存储(服务端只签发预签名 URL 和管理元数据),文件流量不经过业务服务器,避免占满带宽与连接。

🌳 高级:成本与治理

  1. 分片大小取舍:太小 → 请求数多、元数据大;太大 → 失败重传代价高。经验值 5–20MB,网络差时自适应调整;
  2. 垃圾回收:未完成的上传会一直占用存储并产生费用,官方也建议配置生命周期规则清理未完成的分片上传,我们要有同样的定时清理机制;
  3. 安全:预签名 URL 设短过期、限制大小与类型;上传后异步做病毒扫描/内容审核,通过前不可访问;
  4. 去重存储:按内容哈希存储(内容寻址),多用户共享同一物理文件,引用计数删除;
  5. 容量估算示例:日上传 10 万个平均 200MB 文件 = 20TB/天,带宽峰值按 8 小时集中约 5.5Gbps,必须直传对象存储 + CDN 回源优化。

十、订单超时关闭

🌱 初级:几种方案对比

方案做法优点缺点
定时任务扫表每分钟扫描 status=待支付 and create_time < now-30min最简单、可靠有延迟;数据量大时扫表压力大(需索引 + 分页)
MQ 延迟消息下单时发一条延迟消息,到时消费并检查订单状态准时、无需扫表依赖 MQ 延迟能力(如 RocketMQ 5.x 延迟消息默认最长 24 小时)
Redis 过期监听key 过期事件通知看起来简单不可靠:通知可能丢失,不建议用于关键业务
Redis ZSET 延迟队列score=到期时间,轮询取到期元素轻量要自己处理重复消费、宕机恢复
时间轮内存环形数组,槽位放任务高效,适合海量定时任务进程内,重启丢失,需要持久化配合

🌿 中级:关闭动作必须幂等且与支付竞争安全

最经典的坑:用户刚好在超时那一刻支付成功。处理方式:关闭订单与支付成功都用条件更新竞争同一个状态字段——

-- 关闭:只有仍是"待支付"才能关,影响行数=1 才继续回补库存/优惠券
UPDATE orders SET status='CLOSED' WHERE order_no=? AND status='UNPAID';
-- 支付成功:只有仍是"待支付"才能变成已支付
UPDATE orders SET status='PAID' WHERE order_no=? AND status='UNPAID';

谁先更新成功谁赢,另一个影响行数为 0 就放弃。若关单先赢而随后收到支付成功回调,则走"自动退款"或"订单复活"流程,并且记录日志用于对账。关闭时回补库存、释放优惠券、解冻余额,这些动作都要幂等(用订单号作唯一标识),失败进入重试队列。

🌳 高级:海量订单与兜底

  1. 推荐组合:延迟消息为主 + 扫表兜底。延迟消息保证准时,定时扫描(例如每 5 分钟)捕捉因消息丢失或异常遗漏的订单,两者靠状态条件更新天然幂等,谁先谁后都不会出错;
  2. 分片扫描:订单表分库分表后,扫描任务按分片并行;分页按主键游标(id > ? order by id limit 500),不要用大 offset;扫描 SQL 要走 (status, create_time) 联合索引;
  3. 延迟消息的雪崩风险:大量订单同一秒到期(如整点活动),消费瞬时积压。官方文档也提示大量消息同一投递时间会造成压力。做法:延迟时间加随机抖动(如 30 分钟 ± 30 秒);
  4. 业务上的时间语义:支付渠道自身也有支付超时,订单关闭时间应略大于渠道支付超时,或关闭前主动向渠道查询支付状态,避免"用户其实付了款、我们把单关了"。

十一、高频追问速查表

问题一句话答案出处/延伸
系统设计怎么开场先澄清需求与规模,再估算,再画主流程,最后讲瓶颈与取舍第一节框架
秒杀如何防超卖Redis Lua 原子预扣 + DB 条件更新 num>=1 兜底第二节
秒杀 Redis 挂了暂停活动或降级为网关限流 + DB 条件更新,宁可慢不可错第二节高级
短链 301 和 302301 被缓存无法统计;需要统计/可下线用 302第四节
排行榜大 key只留 TopN;分片榜 + 归并;名次近似估算第五节
红包怎么拆金额二倍均值法,最后一人拿剩余,每人至少 1 分第六节
红包资金安全流水只追加 + 守恒公式定时校验 + 幂等入账/退款第六节高级
扫码登录安全二维码一次性短 TTL、扫描与确认分离、ticket 用后即焚第七节
10 亿数据去重位图/布隆过滤器;容忍误差用 HyperLogLogRedis 官方 HLL 文档
海量数据 TopK哈希分片 → 局部统计 → 小顶堆 → 归并第八节
大文件上传分片 + 断点续传 + 秒传 + 直传对象存储S3 分片上传文档
订单超时关闭延迟消息为主 + 扫表兜底,状态条件更新保证幂等第十节
Redis 过期监听能用吗不可靠,关键业务不要依赖第十节

一句话备忘:系统设计 = 澄清 → 估算 → 主流程 → 瓶颈 → 取舍 → 兜底;把无效流量挡在最前面,把钱和库存放在有条件更新的数据库里做最终裁决,把所有异步链路配上对账。

最后说句实在话:系统设计面试没有"标准答案",但有"糟糕答案"——就是只报组件不讲取舍、不讲出错怎么办。哪怕你的方案朴素,只要能讲清楚数量级和退路,面试官通常就愿意继续往下聊。祝你面得从容。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人