系统设计面试
系统设计面试
【困难】如何设计一个秒杀系统?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / 秒杀
💎 关键结论
秒杀应对的是瞬时海量请求,思路是「稳、准、快」:前端静态页 + CDN 挡读流量,答题 + 限流削峰,扣库存用 Redis + Lua 原子操作保证不超卖,只有扣减成功的请求进 MQ 异步建单。最终 DB 写入量等于库存量,而不是请求量。
⚡记忆卡片
- 口诀:静态挡、答题缓、限流削、Lua 扣
- 关键词:瞬时海量请求 / 超卖 / 分层过滤 / Redis+Lua / 异步建单
- 链路:前端静态 + CDN → 答题限流 → Redis Lua 扣库存 → MQ 削峰 → 异步建单 → 对账兜底
📖 核心知识
秒杀系统所要应对的场景是:瞬时海量请求。
1. 秒杀的难点
- 高并发:可细分为二:
- 并发读:读取剩余库存量以及商品信息;
- 并发写:下单后写入订单记录。
- 超卖:秒杀商品通常性价比极高甚至赔本赚吆喝,库存 100 件若瞬时下单超过 100 件且处理不当,就会超卖,给商家带来巨大经济损失。
- 恶意请求:有人在多台机器上跑脚本,模拟大量用户抢商品。
- 数据库崩溃:没有 MQ 削峰、没有过载保护,海量请求直接打到数据库,数据库挂掉还会波及其他业务,让整个系统瘫痪。
- 对现有业务造成冲击:秒杀本质是营销活动,不能影响主站。
2. 设计目标:稳、准、快
- 稳(高可用):架构要能撑住活动;
- 准(一致性):减库存方式不能出现超卖;
- 快(高性能):从前端到后端、依赖组件协同优化。

3. 前端优化
- 静态页面:秒杀商品页面静态化,减少查库 IO;静态页做 CDN 缓存,前后端分离时还可在反向代理服务器侧设置静态缓存。商品由 ID 标识(如
http://item.xxx.com/item.htm?id=xxxx),对应页面可提前做前端缓存,无需查商品信息。 - 按钮控制:活动开启前下单按钮禁用;点击后禁用一段时间,防止疯狂输出。
4. 后端优化
- 限流、熔断、降级、隔离:秒杀不应影响现有业务——
- 隔离:秒杀系统、数据与其他正常业务彼此隔离,互不影响;
- 限流:设置阈值,超过阈值拒绝请求,防止数据库被打死;
- 降级:保证核心业务继续工作,非核心业务各安天命;
- 熔断:不影响别的系统。
- 多级缓存:缓存要预热,避免瞬间流量冲击;雪崩、穿透、击穿的常规处理要做好;缓存自身也要高可用。
- 流量削峰:排队、答题、分层过滤——
- 排队:用消息队列缓冲瞬时流量,但 MQ 自身有上限,积压过多也处理不了;
- 答题(摇一摇):限制秒杀器并延缓请求;
- 分层过滤:漏斗式设计,尽可能拦截无效请求。

- 减库存:
- 恶意下单:结合安全与反作弊——识别频繁下单不付款或重复下单不付款的用户并阻断;限制个人购买数。
- 避免超卖:核心是保证高并发下库存字段不为负数,常见方案:
- 应用程序中通过事务判断,减后库存为负则回滚;
- 数据库字段设为无符号整数,减后小于零直接报错;
- 使用 CASE WHEN 条件更新:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END- 库存是交易环节的关键数据也是热点数据,但秒杀不需要精确的一致性读,把库存放到缓存(Cache)中可大大提升读性能。
- URL 动态化:通过 MD5 之类的加密算法加密随机字符串生成 URL,前端获取后经后台校验才能通过,防止提前抢购。
🔬 扩展知识
详情
- 【L3】漏斗模型量化(以 100w QPS 峰值、10w 库存为例):静态页 CDN 挡掉 99% 读请求 → 答题/验证码滤掉 90% → 网关限流到应用承载量 → Redis 扣库存后仅扣减成功的请求进 MQ 建单。最终 DB 写入量 = 库存量(10w),而非请求量(100w)。
- 【L3】库存扣减方案对比:Redis + Lua 原子扣减(主流,
if stock >= n then decr,扣成功才发 MQ 建单,DB 异步落库,需缓存与 DB 对账兜底);数据库乐观锁 CAS(UPDATE stock SET n = n - ? WHERE id = ? AND n >= ?,简单可靠但热点行锁竞争大,适合库存量小、并发中等的场景);分段库存(把 10w 库存拆成 10 个 key 各 1w,随机路由打散热点,需处理段间余量不均——某段扣完可借调或跳转其他段)。 - 【L3】热点隔离与对账:秒杀商品用独立 Redis 实例/独立 DB 库表,与主站数据物理隔离,活动结束资源回收;Redis 库存与 DB 库存定时对账,不一致以 DB 为准修复。
- 【L4】方案权衡:Redis + Lua 扣减性能最高(单实例 8~10 万 QPS)但引入缓存与 DB 双存储一致性问题,需对账兜底;纯 DB 乐观锁无一致性问题但热点行锁竞争下 QPS 只有几千;分段库存解决热点但引入段间不均的管理复杂度。选型看库存量:库存 100 件没必要上 Redis,库存 10 万 + 峰值十万级才需要。
- 【L4】失效场景:MQ 建单积压超容量时用户"抢购成功但无订单",体验崩塌,需积压监控 + 提前告知;答题/验证码环节若被脚本破解,漏斗失效,需风控实时升级难度。
📚 延伸阅读:如何设计一个秒杀系统、一个秒杀系统的设计思考
🏭 实战场景
详情
某平台周年庆秒杀:100 个 SKU,每个库存 1000 件,预计峰值 50 万 QPS 持续 3 秒,要求不超卖、不影响主站交易。
设计要点:漏斗设计——页面静态化 + CDN 挡掉 95% 读流量,答题/验证码滤掉 90% 无效请求,剩余约 2 万 QPS 进应用层;单 SKU 库存 1000 件无需分段,Redis Lua 原子扣减即可(总写压力 5 万 QPS 内,单 Redis 实例可扛);扣减成功的 10 万件请求进 MQ 异步建单,DB 写入被平滑到几分钟。独立部署秒杀集群 + 独立库表与主站隔离;全链路限流兜底,DB 前置 3000 QPS 保护。
⚠️ 常见误区
详情
常见误区:
- ❌ "在网关加个限流,数据库就不会被打挂" → 限流只控制流速,不减少放大效应;不做静态页 + CDN 拦截,读请求照样压到缓存/DB,必须漏斗式分层过滤。
- ❌ "应用层先查库存再扣减就能防超卖" → 查与扣不是原子操作,高并发下必然超卖,必须用条件更新 SQL 或 Redis + Lua 原子脚本。
- ❌ "秒杀和主站共用部署,加限流就行" → 秒杀流量瞬时放大十万倍,共享资源时拖垮主站交易是大概率事件,应独立部署、隔离故障域。
🔀 发散问题
- Q:库存扣完后的几十万"已失败"请求怎么低成本地告诉用户? → 库存为 0 后置位 Redis 标志,网关/应用层直接短路返回"已售罄",不再走后续链路;前端按钮置灰 + 轮询/推送结果,避免用户反复刷。
- Q:Redis 扣库存成功但建单失败(MQ 丢消息)怎么办? → 扣库存与发 MQ 无法原子,需兜底:建单成功后回写"已建单"标记,定时任务扫描"已扣减但未建单"的记录回补库存或重建订单;同时 Redis 与 DB 对账修正总量。异步建单的状态一致性设计与订单系统相关,见本文档「如何设计一个订单系统?」。
- Q:秒杀扣库存和优惠券防超发有何异同? → 同为 Redis Lua 原子扣预算 + 异步落库 + 对账兜底,但优惠券还要叠加单用户限领与规则互斥校验,见本文档「如何设计一个营销/优惠券系统?」。
【中等】如何设计一个文件上传系统?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:15 min | 🏷 标签:系统设计 / 文件存储
💎 关键结论
文件上传核心是四件事:存储选型、大文件分片、去重秒传、限流保护。小厂直接用 OSS 云服务,中大厂可基于 FastDFS、MinIO 自建;超大文件切片并行上传并支持断点续传,用文件哈希判重实现秒传,再用令牌桶限流防带宽被刷。
⚡记忆卡片
- 口诀:大分片、重哈希、断续传、刷限流
- 关键词:OSS/MinIO / 分片上传 / 断点续传 / 哈希秒传 / 令牌桶
- 链路:客户端切片 → 哈希判重 → 并行上传 → 服务端合并 → 限流保护
📖 核心知识
1. 存储选型
- 小厂可直接用 OSS 云服务,如阿里云 OSS 或腾讯云 OSS;
- 中大厂可基于 FastDFS、MinIO 等服务自建。
2. 超大文件上传
| 方案 | 实现 | 优势 |
|---|---|---|
| 分片上传 | 客户端切分成片 (1-10MB),并行上传,服务端合并 | 突破单文件大小限制,并行加速 |
| 流式上传 | 边读边传,不落本地磁盘 | 节省客户端内存 |
| 分段合并 | 服务端接收分片,异步合并 | 避免长连接占用 |
3. 断点续传
- 客户端:记录已上传分片列表(localStorage/IndexedDB);
- 上传前:请求服务端获取已上传分片列表;
- 上传时:只传缺失分片;
- 完成:服务端合并。
4. 避免重复文件存储
- 客户端计算文件数字签名(采用 MD5、SHA 等算法);
- 如果文件特别大,可按固定大小切片,针对每个切片取头部固定数量的字节生成签名(即分片哈希);
- 服务端查询文件哈希值是否存在:
- 存在:直接返回已有文件 URL(0 秒完成);
- 不存在:执行正常上传。
哈希碰撞:碰撞概率极低,但不可不防——
- 文件数字签名采用尽可能安全的哈希算法,如 SHA-256;
- 数字签名相同时再对比文件大小,大小不同一定不是同一文件;
- 还可以抽取文件的部分字节内容进行比对。
注意:需记录文件被引用次数,引用数为零时物理删除。文件哈希这种思路不仅可用于避免重复存储,也可用于秒传——发现上传的是重复文件,直接返回上传成功。
5. 限流
限流策略:
- 单用户:10MB/s 或 100 个文件/小时;
- 单 IP:3 个并发上传;
- 单文件大小:100MB(图片)/1GB(视频);
- 集群总带宽:1Gbps(Nginx 限速)。
实现:令牌桶/漏桶算法 + Redis 计数器。
6. 性能设计
| 优化点 | 方案 | 效果 |
|---|---|---|
| 分片并发 | 3-5 片并行上传 | 带宽利用率提升 |
| 压缩传输 | Gzip 压缩文本文件 | 传输量减少 70% |
| CDN 加速 | 上传域名走 CDN 动态加速 | 跨地域延迟降低 |
| 就近上传 | DNS 解析就近上传节点 | 减少网络往返 |
| 内存池 | 复用缓冲区 | GC 压力降低 |
🔬 扩展知识
详情
- 【L3】为什么大文件要用分片哈希?全量文件哈希计算耗时随文件大小线性增长,按分片取头部固定字节生成签名可快速判重;安全上应选 SHA-256 等抗碰撞算法,MD5 已不建议用于安全判重。
- 【L3】容量与带宽估算:例如单文件上限 1GB、日活上传者 1 万、人均上传 100MB,日增存储约 1TB(3 副本则约 3TB);上传集群出口带宽需覆盖并发上传数 × 单用户限速,1000 路并发 × 10MB/s 约需 80Gbps,据此反推节点数。
- 【L4】合并失败与一致性:分片合并耗时长,应异步合并 + 任务表记录 + 失败重试;分片完整性以服务端记录为准,避免"合并完成但分片缺失"的幽灵文件。自建分片上传的本质与云厂商 OSS 的 Multipart Upload API 一致,可参考其分片号、回调与断点协议设计。
🔀 发散问题
- Q:如何防止恶意用户刷上传带宽? → 多维度限流:单用户限速/限文件数、单 IP 限并发、单文件大小上限、集群总带宽(Nginx 限速),用令牌桶/漏桶 + Redis 计数器实现。
- Q:秒传具体怎么实现? → 客户端计算文件哈希先查服务端,命中则直接返回已有文件 URL(0 秒完成),只增加一次引用计数,不做物理存储。
- Q:哈希碰撞了怎么办? → 碰撞概率极低但必须设防:用 SHA-256 等安全算法;签名相同时再比文件大小;必要时抽样部分字节比对。
【中等】如何设计一个单点登录系统(SSO)?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:15 min | 🏷 标签:系统设计 / 认证
💎 关键结论
SSO 核心是统一认证中心 + 可信凭证,子系统不存用户信息,只校验凭证。选型上,前后端分离/微服务用 JWT,传统 Web 用 CAS,第三方授权用 OAuth2.0;关键保障是凭证防篡改、登出同步、认证中心多活,避免单点故障。
⚡记忆卡片
- 口诀:统一认证、凭证可信、本地验签、黑名单登出
- 关键词:认证中心 / JWT / CAS 票据 / Refresh Token / 黑名单
- 链路:子系统跳转 → 认证中心登录 → 颁发凭证 → 本地验签免登 → 黑名单 + 通知登出
📖 核心知识
1. 核心组件
| 组件 | 作用 |
|---|---|
| 认证中心(SSO Server) | 唯一登录入口,负责用户身份校验、颁发凭证、登出同步 |
| 子系统(SSO Client) | 业务系统(如订单、商品、支付系统),依赖认证中心验证用户身份 |
| 凭证(Token/Ticket) | 认证中心颁发的身份标识(如 JWT、ST 票),用于子系统校验 |
| 用户 | 访问各子系统的终端用户 |
2. 步骤一:选择 SSO 实现方案
| 方案 | 核心逻辑 | 适用场景 |
|---|---|---|
| Token 方案(JWT) | 认证中心生成 JWT Token,子系统本地验签(无需回调认证中心) | 前后端分离、微服务、跨域(APP / 小程序) |
| CAS 协议(票据) | 子系统跳转认证中心登录,认证中心返回 ST 票,子系统回调认证中心验票 | 传统 Web 系统、需严格会话控制 |
| OAuth2.0(授权) | 子系统申请授权,认证中心颁发 Access Token,用于访问用户资源 | 第三方授权登录(如微信 / 支付宝登录) |
3. 步骤二:设计凭证机制(凭证防篡改)
- JWT Token(推荐):
- 结构:Header(加密算法)+ Payload(用户 ID / 过期时间)+ Signature(签名);
- 核心:子系统用公钥验签,无需调用认证中心,性能高;
- 防护:设置短过期时间(如 1 小时),配合刷新令牌(Refresh Token)续期,Token 不可篡改。
- 票据(CAS 方案):
- TGT:用户登录后认证中心生成的会话凭证(存服务端);
- ST:子系统跳转时,认证中心基于 TGT 生成的一次性票据,子系统回调认证中心校验 ST 有效性。
4. 步骤三:设计登录流程(以 JWT 为例)
- 用户访问子系统 A,未登录则跳转至认证中心;
- 用户在认证中心输入账号密码,校验通过后,认证中心生成 JWT Token(含用户 ID、过期时间),返回给用户;
- 用户携带 Token 访问子系统 A,子系统 A 验签通过后,建立本地会话,实现登录;
- 用户访问子系统 B,携带同一 Token,子系统 B 验签通过后,直接免登。
5. 步骤四:设计登出流程(关键防残留)
- 用户在任一子系统发起登出请求,跳转至认证中心;
- 认证中心处理:若用 JWT,将 Token 加入 Redis 黑名单(解决 JWT 无法主动销毁);若用 CAS,销毁服务端 TGT 会话;
- 认证中心通知所有子系统销毁本地会话(如通过 MQ 推送登出消息);
- 子系统校验 Token 时,先查黑名单,失效则拒绝访问。
6. 步骤五:核心防护设计(必做)
- 防伪造:Token 签名加密(如 HS256/RSA),避免篡改;
- 防过期:Refresh Token 机制,Token 过期后自动刷新;
- 防跨域:配置 CORS 白名单,仅允许可信子系统访问;
- 防重放:Token 加入 nonce 随机数,或设置极短过期时间;
- 高可用:认证中心集群部署,Token 黑名单(Redis)做主从同步。
🔬 扩展知识
详情
- 【L3】JWT 无状态的代价:登出/踢人/改密无法立即生效,本质是用黑名单把无状态又变成了有状态——黑名单 key 只存 jti 不存全量 Token,TTL 设为 Token 剩余有效期,控制 Redis 内存。
- 【L3】时钟漂移:验签时 exp 判断加几秒容差(leeway),否则集群机器时钟不同步会导致 Token 忽有效忽失效。
- 【L4】单点登出的最终一致窗口:MQ 通知各子系统销毁会话是异步的,存在秒级窗口;可接受则用短有效期 Token 兜底,不可接受则子系统每次请求回调认证中心校验(牺牲性能换强控)。
- 【L4】方案权衡与事故:JWT 验签无需回调认证中心,每请求省去一次远程调用(约 5~10ms),代价是无法即时废黜(依赖黑名单);CAS 会话集中可控但认证中心成为性能瓶颈与单点,高并发下验票压力需集群抗。曾有线上事故:认证中心单点宕机,所有子系统登录态校验失败,全站不可用 20 分钟——认证中心必须多活 + 子系统本地验签兜底。
- 【L4】规模化估算(集团 20 个子系统、Web + App + 小程序混合、日登录 500 万次、踢人 10 秒内生效):选 JWT + Refresh Token(App/小程序无法用 Cookie 共享),认证中心多活部署 + Redis 黑名单集群;踢人靠黑名单 jti 秒级生效,子系统本地验签缓存 TTL 设 5~10 秒兜底;登录 QPS ≈ 500 万/86400 × 峰值系数 ≈ 几百 QPS,两机房双活轻松承载。关键设计:子系统对认证中心的依赖只有登录/刷新,验签本地完成,认证中心短时抖动不影响存量会话。
🔀 发散问题
- Q:JWT 方案下如何实现"踢人下线"? → 把被踢用户的 jti 或 userId + 签发时间戳加入 Redis 黑名单(TTL = Token 剩余有效期),子系统验签后查黑名单;若子系统本地缓存了验签结果,需 MQ 广播失效事件,容忍秒级窗口。
- Q:跨域场景(app.xxx.com 与 h5.xxx.com)如何共享登录态? → 同主域用 Cookie domain=.xxx.com 共享;跨主域只能走认证中心重定向(CAS 式)或 Token 通过 URL/PostMessage 传递后存本地,后者需防 URL 泄露(一次性 code 兑换)。
- Q:认证中心 QPS 怎么估算? → JWT 方案下认证中心只承担登录/刷新请求(QPS = 登录频次,远低于业务 QPS);CAS 方案要按全量请求验票估算,通常需本地 Session 缓存把验票频率降到会话级,否则认证中心会成为全链路瓶颈。
- Q:微信扫码登录和 SSO 是什么关系? → 微信扫码属于第三方授权登录(OAuth2.0 授权码模式),可视为 SSO 体系中接入外部认证中心的一种形态,见本文档「如何实现微信扫码登录?」。
【中等】如何实现微信扫码登录?⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:15 min | 🏷 标签:系统设计 / 第三方登录
💎 关键结论
微信扫码登录本质是 OAuth2.0 授权码模式:PC 端生成 UUID 画成二维码,用户扫码确认授权,微信服务器绑定 UUID 与 openid 并回调,PC 端轮询到授权状态后用 code 换 AccessToken、拉取用户信息,最后颁发自己的 Session 或 JWT 完成登录。
⚡记忆卡片
- 口诀:画码、扫绑、轮询换、取信登
- 关键词:UUID / openid / 授权码 code / AccessToken / 轮询
- 链路:生成 UUID 二维码 → 扫码绑定 openid → 回调更新状态 → 轮询得 code → 换 Token → 颁发会话
📖 核心知识
1. 第一阶段:准备阶段(生成二维码)
目标:让 PC 网页拿到一个唯一标识本次登录请求的 UUID,并把它画成二维码。要点:网页后端生成唯一 ID,Redis 记下它状态,URL 塞进二维码里。
- 准备材料:第三方网站需要在微信开放平台提前注册,拿到
AppID和AppSecret; - 请求二维码:用户点击"微信登录",PC 端网页向后端请求一个二维码;
- 生成 UUID:第三方服务器生成一个唯一的、临时的 UUID(或称 state/ticket),并把该 UUID 的状态(如:未扫描)存到 Redis;
- 返回二维码:服务器把包含
AppID、redirect_uri(回调地址)和该UUID的信息包装成一个 URL,返回给前端画成二维码。
2. 第二阶段:扫描阶段(扫码确认)
目标:用户扫码确认,让微信服务器知道"这个 UUID 被这个微信号授权了"。要点:手机扫码读 UUID,微信确认发授权,绑定 UUID 和 openid。
- 扫码:用户打开微信扫一扫,微信 APP 解析出二维码中的
UUID; - 跳转授权:微信 APP 将该
UUID和用户身份信息(openid)发送给微信服务器,并展示授权页面("XX 应用请求获取你的昵称、头像"); - 用户确认:用户点击"确认登录";
- 记录绑定:微信服务器记录
UUID与openid的绑定关系,并通过回调通知第三方服务器该UUID已被授权。
3. 第三阶段:回调阶段(登录成功)
目标:PC 网页得知授权成功,拿到用户信息,完成登录。要点:前端一直轮询,发现授权赶紧换,换到 Token 取信息,生成票据登录。
- 轮询查状态:扫码的同时,PC 前端一直在轮询后端接口:"我这个
UUID的状态变了吗?"; - 状态变更:微信服务器通知授权成功后,第三方服务器把 Redis 中该
UUID的状态更新为"已授权",并准备好code(授权码); - 换取 Token:PC 前端轮询到"已授权",后端立即使用
code+AppSecret去微信服务器换取AccessToken; - 获取用户信息:用
AccessToken调用微信接口,获取用户openid、昵称、头像等信息; - 登录成功:后端用这些信息生成自己系统的
Session或JWT Token返回给前端,登录完成。
🔬 扩展知识
详情
- 【L3】为什么用 code 换 Token,而不是直接把 Token 返回给前端?code 是一次性、短时效的前通道凭证;AccessToken 由后端用 code + AppSecret 通过服务端通道换取,即使 code 被截获,没有 AppSecret 也换不到 Token,避免敏感凭证暴露在前端。
- 【L3】轮询 vs 长连接:轮询实现简单、兼容性好(间隔 1~2 秒并限制总时长),UUID 状态存 Redis 并设置 TTL(如 5 分钟);大规模场景可升级为 WebSocket/SSE 推送,减少无效轮询请求。
- 【L4】安全要点:UUID 必须随机不可预测,防止他人轮询劫持扫码结果;redirect_uri 需在开放平台配置白名单校验,防止授权码被钓鱼到恶意站点;OAuth2.0 实践中还应带 state 参数防 CSRF。
🔀 发散问题
- Q:二维码超时未被扫描怎么办? → UUID 状态设置 TTL,过期后端置为失效,前端提示"二维码已失效"并提供刷新按钮,重新生成 UUID 与二维码。
- Q:扫码登录成功后,PC 端与移动端如何维持统一登录态? → PC 端登录成功后颁发自有 Session/JWT,后续多端登录态维持就是 SSO 问题,见本文档「如何设计一个单点登录系统(SSO)?」。
- Q:授权码模式和隐式模式(直接返回 Token)有什么区别? → 授权码模式的 Token 走后端通道换取,更安全;OAuth2.0 当前最佳实践推荐授权码模式,不推荐把 Token 直接返回给前端的隐式模式。
【中等】如何设计一个短链服务?⭐⭐⭐⭐
🎯 目标等级:L2-L3 | ⏱ 建议用时:15 min | 🏷 标签:系统设计 / 短链
💎 关键结论
短链服务三个字:生成、存储、重定向。主流方案是自增 ID 经发号器生成后转 62 进制短码,写 MySQL 读 Redis 缓存,用 302 跳转保留点击统计能力;典型读多写少,缓存可挡 99% 请求,布隆过滤器防缓存穿透。
⚡记忆卡片
- 口诀:发号取数、62 转码、缓存挡读、302 统计
- 关键词:发号器 / 62 进制 / Redis 缓存 / 302 / 布隆过滤器
- 链路:长链 → 发号器 ID → 62 进制短码 → 写 MySQL → 读缓存 → 302 跳转
📖 核心知识
要点:生成、存储、重定向。
1. 生成:长链变短码
- 哈希:对长链接进行哈希(如
MD5),得到一个 32 位字符串; - 自增 ID:用一个中心服务(如 Redis)生成自增数字 ID(如:100001、100002)。
短链不够短可采用转码:把数值转成 62 进制(用光所有数字 + 大小写字母)。例如 ID 100001 → 短码 aB3。
2. 存储:写 DB,读 Cache
- 写路径:
长链接 -> 发号器拿 ID -> 转短码 -> 存 MySQL(落盘); - 读路径(关键):第一步查 Redis,有就直接用;Redis 没有查 MySQL;查到后回写 Redis,下次就快了。
3. 重定向:301 还是 302?
- 301(永久):浏览器会记住,下次不再访问短链服务器;缺点是没法统计点击次数;
- 302(临时):每次都先找短链服务器再跳转;优点是可以精确计数(点了几次、谁点的)。
- 结论:推荐 302,为了数据和灵活性。
4. 性能设计
- 缓存:缓存挡住 99% 的读请求,不让流量打垮 DB;
- 预发号:短链生成服务不要每次都找发号器,而是一次性生成一批短链(比如 1000 个)放本地内存,用完了再拿,减少对发号器的压力;
- 布隆过滤器:恶意输入不存在的短码(如
aaaaaa)会造成缓存穿透,布隆过滤器像前置"筛查员",快速判断短码肯定不存在并直接拦截,保护后端。
🔬 扩展知识
详情
- 【L3】容量估算:62 进制 6 位短码空间 = 62^6 ≈ 568 亿;日生成 100 万短链可支撑约 155 年,日生成 1000 万约 15 年,不够则升 7 位。
- 【L3】发号器高可用:号段模式,每台发号实例批量取号(如每次 1000 个),DB 只存 max_id,多实例取不同号段防冲突;或用 Redis INCR 但需解决持久化。
- 【L3】哈希冲突:MD5 取前 6 位冲突概率高,需查库判重后重算或拼接重试;自增 ID 转 62 进制无冲突,是更稳的选择。
- 【L4】防枚举与防爬:自增 ID 可被顺序枚举,可对 ID 做位运算混淆或加随机盐后再转 62 进制;监控异常短码扫描行为。
- 【L4】方案权衡与事故:哈希方案无需中心化发号但冲突处理复杂(查库重试),高并发下冲突重试放大延迟;自增 ID 方案无冲突但发号器是单点依赖,需号段模式消除。曾有线上事故:用 MD5 前 6 位做短码未做冲突检测,两条不同长链映射同一短码,导致营销活动跳转到错误落地页——哈希取短必须判重,或直接选自增 ID 方案。
- 【L4】失效场景与大规模估算:布隆过滤器存在误判率(如 1% 误杀真实短码),需控制容量与哈希函数数量;短链服务典型读多写少(100:1 以上),读缓存失效风暴时需互斥重建防击穿。大促示例(日生成 500 万、峰值 5 万 TPS 生成、读峰值 20 万 QPS、防枚举):发号器号段模式多实例(每实例每次取 1 万号段,DB 压力仅几百 QPS);防枚举用自增 ID 先乘一个大质数再异或固定盐后转 62 进制,短码与 ID 无单调关系;读路径 Redis 集群承载(20 万 QPS 约 2~3 个分片实例),长 TTL 命中率 99%+,DB 只做兜底;生成峰值用 MQ 削峰异步落库,同步只返回预取号段生成的短码。
🔀 发散问题
- Q:短链服务读 QPS 10 万、缓存命中率 99%,DB 要承受多少?如何再降? → 回源约 1000 QPS,单 MySQL 实例可扛;若命中率下降可加本地缓存(短码映射几乎不变,本地缓存命中率极高)再降一个数量级。关键设计:短码 → 长链是不可变映射,缓存可设长 TTL 甚至永不过期。
- Q:如何统计点击数据而不拖慢跳转? → 302 返回与统计异步解耦:跳转响应先发,点击事件丢 MQ 异步落库/实时计算;统计分析走独立链路(如 Flink 实时聚合),不在跳转主路径同步写 DB。
- Q:发号器实例宕机,预取的号段会怎样? → 号段在实例内存,宕机丢未用完的号段(如 1000 个),ID 出现空洞但不重复——号段模式天然接受空间浪费换可用性;双 Buffer 下另一个号段仍可用,对业务透明。
【困难】如何设计一个 Feed 流系统(朋友圈/微博)?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / Feed 流
💎 关键结论
Feed 流本质是读写扩散的权衡:推模式发帖时写入粉丝收件箱,读快写慢,适合普通用户;拉模式读时合并,写快读慢,适合大 V;业界主流是推拉结合。存储用 Redis ZSet 存最近 N 条帖子 ID,历史落盘,读时批量回填内容。
⚡记忆卡片
- 口诀:小粉推、大 V 拉、读时合并、ZSet 存
- 关键词:写扩散 / 读扩散 / 推拉结合 / ZSet / 批量回填
- 链路:发帖 → 扩散决策(推/拉)→ 收件箱 ZSet → 读时合并 → ID 回填内容
📖 核心知识
Feed 流核心问题:用户关注 N 个人,如何快速拉出按时间排序的内容流?本质是读写扩散的权衡。
1. 三种模式对比
| 模式 | 原理 | 写开销 | 读开销 | 适用 |
|---|---|---|---|---|
| 推模式(写扩散) | 发帖时写入所有粉丝的收件箱 | 高(粉丝数倍数) | 低(直接读自己的箱子) | 粉丝数小的普通用户 |
| 拉模式(读扩散) | 读时遍历关注人的发件箱合并 | 低 | 高(关注数倍数) | 大 V(千万粉丝) |
| 推拉结合 | 普通用户推,大 V 拉,读时合并 | 中 | 中 | 业界主流(微博) |
量化选型:某用户 100w 粉丝,每天发 10 条 → 纯推模式日写 1000w 条,不可接受;改用拉模式后只在粉丝读时合并。
2. 存储设计
- 收件箱/发件箱:Redis ZSet(score = 发帖时间戳,member = 帖子 ID),只存最近 N 条(如 1000);历史数据落 MySQL/HBase;
- 帖子内容单独存储(帖表),Feed 流只存 ID,读时批量回填(注意用 batchGet 避免 N+1)。
3. 其他要点
- 排序策略:时间序 vs 算法推荐;
- 幂等:客户端生成帖子 ID 防重发;
- 删帖:需异步清理已扩散的收件箱(或读时过滤已删帖子)。
🔬 扩展知识
详情
- 【L3】推拉分界线怎么定:按"写放大 vs 读放大"成本平衡——某用户粉丝 F、日发帖 P,推成本 = F×P 次写/天;关注 K 人的用户拉成本 = K 次合并/刷。一般粉丝超万级转拉模式,具体阈值用实际读写 QPS 比例压测校准,且分界线应可配置化动态调整。
- 【L3】游标分页:以最后一条帖子的时间戳/ID 作为游标,向历史库(MySQL/HBase)拉取更早数据;Redis 只承担"最新一页"的热读,历史读走冷存储,避免 ZSet 无限膨胀。
- 【L4】个性化推荐排序:读时先拉候选集(收件箱 + 大 V 发件箱合并,取最近几百条 ID),再调推荐服务打分重排(召回 + 粗排 + 精排分层);打分服务超时需降级为时间序兜底,不能阻塞 Feed 返回。
- 【L4】方案权衡:推模式的代价是写放大(粉丝数倍数),大 V 发帖一条写百万次,纯推模式在大 V 场景必然失效;拉模式的代价是读放大(关注数倍数),用户关注 500 人时每次刷 Feed 要合并 500 个列表,读 RT 不可接受;推拉结合的分界线(如粉丝 > 1 万走拉)需根据实际读写比例压测定,而非拍脑袋。
🏭 实战场景
详情
朋友圈类场景:用户平均 300 好友(双向关注、无大 V 问题),DAU 1 亿,日发帖 5000 万条,要求刷 Feed P99 < 200ms。
设计要点:好友数小(300)且无超大 V,写扩散可接受——每条帖子平均扩散 300 次写,日写量 5000 万 × 300 = 150 亿次,峰值约 50 万写 QPS,需 Kafka 削峰 + Redis 集群(分片)承载;读路径直接取自己收件箱 ZSet,单次读取 O(1) 命令,P99 可控。权衡点:若写集群成本过高,可退为"活跃用户推、休眠用户拉"——发帖时只扩散给近 30 天活跃好友,大幅削减无效写。
行业教训:某社区纯推模式上线,千万粉大 V 一条帖子触发千万次 Redis 写入,收件箱写队列积压数小时——写扩散必须有粉丝数上限。
⚠️ 常见误区
详情
常见误区:
- ❌ "Feed 流一律用推模式,读性能最好" → 大 V 发帖一次写百万粉丝收件箱,写放大会打垮存储,必须对大 V 切换拉模式。
- ❌ "收件箱里直接存帖子内容,读起来省事" → 扩散会让内容被复制千万份,存储爆炸且更新困难,应只存帖子 ID,读时批量回填。
- ❌ "读扩散慢就多加机器" → 读 RT 随关注数线性增长,关注 500 人就要合并 500 个列表,单请求串行合并的代价加机器解决不了,需并行合并 + 超时截断。
🔀 发散问题
- Q:用户关注太多导致 Feed 读取超时怎么办? → 拉模式并行合并 + 超时截断(时限内返回已合并的 Top 部分);或限制有效关注数、只拉取近期活跃关注者的发件箱。
- Q:删帖后已扩散到百万收件箱的引用怎么清理? → 同步清理会被扩散量拖垮,应异步任务清理;清理完成前读时过滤已删帖子兜底。
- Q:Feed 流存储和 IM 消息存储有什么共性? → 都是"时间线索引 + ID 回填写"模型,用 ZSet/列表存 ID、读时回填内容;IM 还多了会话级 seq 保序与双向扩散问题,见本文档「如何设计一个 IM 即时通讯系统?」。
【困难】如何设计一个 IM 即时通讯系统?⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / IM
💎 关键结论
IM 设计抓四条线:长连接网关无状态水平扩容做接入;ACK + 客户端 msgId + 会话级 seq 保证不丢、不重、不乱序;小群写扩散、大群读扩散;离线消息落库加厂商通道推送。单机网关可撑 10~20 万连接,存储按会话分库分表。
⚡记忆卡片
- 口诀:长连接、ACK 认、seq 序、小推大拉
- 关键词:长连接网关 / ACK / msgId / seq / 读写扩散
- 链路:客户端长连 → 网关路由 → 落库 + ACK → 收件箱扩散 → 离线推送
📖 核心知识
1. 整体架构
接入层(长连接网关)→ 逻辑层(消息路由/会话)→ 存储层(消息库/离线库)+ 离线推送。
2. 接入层设计
- 长连接选型:WebSocket / TCP 自定义协议(Netty);心跳保活(30s)检测断连;
- 连接网关无状态水平扩容,维护 userId → 连接所在网关的映射(存 Redis/注册中心),消息路由靠它找到目标用户的连接。
3. 消息可靠性(不丢、不重、不乱序)
- 不丢:发送方 → 服务器 ACK 确认机制,未 ACK 则重发;服务器 → 接收方同样 ACK;服务器落库成功后再 ACK 发送方;
- 不重:客户端生成全局唯一 msgId(会话 ID + 自增序号),服务端去重;
- 不乱序:每个会话维护自增 seq(Redis INCR 或发号器),客户端按 seq 排序;断线重连后用本地最大 seq 拉取增量补齐。
4. 消息存储与扩散
- 单聊/小群(写扩散):消息写入收发双方的会话索引(inbox),群成员少时逐人写;
- 大群(读扩散):消息只写群消息表,成员读时拉取 + 记录个人已读位点,避免百万人群写扩散爆炸;
- 消息库按会话维度分库分表(uid hash);历史消息归档冷存储。
5. 离线推送
用户不在线时消息存离线库,同时通过厂商通道(APNs/华为/小米)推送唤醒通知;上线后拉取离线消息。
🔬 扩展知识
详情
- 【L3】连接容量量化:单机长连接网关基于 Netty 可支撑 10~20 万连接(受文件描述符与内存限制,每连接内存开销几十 KB);千万在线需百台级网关集群。
- 【L3】seq 发放必须中心化:会话级 seq 要用 Redis INCR 等中心化发号器且按会话维度;用网关单机自增,网关重启后 seq 重复会导致客户端消息乱序重排。
- 【L4】扩散模式权衡与失效场景:百万人群若用写扩散,一条消息写百万条 inbox,写放大不可承受,必须读扩散 + 个人已读位点;厂商推送通道有配额与延迟(秒~分钟级),重要消息需多通道冗余。
🏭 实战场景
详情
企业 IM:500 万员工同时在线(工作日早高峰 9 点集中上线),单聊占 70%、千人群占 5%,要求消息端到端延迟 P99 < 500ms、不丢不乱序。
设计要点:接入层 500 万连接 / 单机 10 万 ≈ 50 台网关(预留 30% 冗余),早高峰重连风暴需客户端指数退避 + 服务端渐进注册;单聊写扩散(收发双方 inbox 各写一条,写放大 2 倍可接受);千人群用读扩散(消息只写群表,成员拉取 + 个人已读位点)——若千人群写扩散,一条消息写 1000 条,早高峰必崩;seq 用 Redis INCR 按会话发号,msgId 客户端生成防重;消息落库成功后再 ACK 发送方,保证不丢。
生产事故教训:曾有线上事故——群消息 seq 用单机自增,网关重启后 seq 重复,客户端消息乱序重排。seq 必须用中心化发号器(Redis INCR)且按会话维度。
⚠️ 常见误区
详情
常见误区:
- ❌ "消息发出去就行,不用 ACK" → 网络抖动与客户端崩溃都会导致消息丢失,必须双向 ACK + 未确认重发,服务端落库成功才能 ACK 发送方。
- ❌ "用服务端生成的消息 ID 就能同时保证去重和顺序" → 重发时服务端会生成新 ID,无法去重;应由客户端生成唯一 msgId 供服务端去重,顺序由独立的会话级 seq 保证。
- ❌ "百万人群用写扩散也没问题,就是多写点" → 一条消息写百万条 inbox,写放大直接打垮存储与写链路,大群必须读扩散。
🔀 发散问题
- Q:消息已读状态怎么设计? → 不逐条标记已读,只存会话级"已读位点"(已读到的最大 seq),未读数 = 最新 seq - 已读位点;群已读回执(如钉钉)是例外,需单独存储已读成员位图,成本高,仅小群可用。
- Q:网关重启时如何做到连接不丢消息? → 客户端断线重连携带本地最大 seq 向新网关拉取增量(写扩散下消息已落库);服务端连接路由表(userId → 网关)随连接迁移更新;核心是消息落库与 ACK 解耦,重连可补拉。
- Q:IM 消息存储量如何估算? → 单条消息约 200~500 字节,日活千万、人均 50 条 → 日增约 5 亿条 / 100~250GB,一年数十 TB;需按会话分库分表 + 半年以上消息归档冷存储(对象存储),在线库只保热数据。
- Q:IM 的时间线存储和 Feed 流有什么共性? → 都是"按会话/用户存 ID 索引 + 读时回填",都需要冷热分层;Feed 流还需处理读写扩散的分界线,见本文档「如何设计一个 Feed 流系统(朋友圈/微博)?」。
【困难】如何设计一个订单系统?⭐⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / 交易
💎 关键结论
订单系统核心是状态机 + 快照 + 分片:状态变更必须带前置条件更新防非法流转,下单时快照商品名与价格隔离后续改价,按买家 ID 分库分表支撑 C 端查询,卖家多维查询靠 ES 异构索引,一致性靠事件驱动 + 对账修复,不追求强一致。
⚡记忆卡片
- 口诀:状态机管流转、快照锁价格、买家分片、ES 查卖家、对账兜底
- 关键词:状态机 / 订单快照 / 买家 ID 分片 / 异构索引 / 事件驱动对账
- 链路:建单(快照 + 预占库存)→ 支付回调异步 → 状态机流转 → 买家分片查 / ES 多维查 → 对账修复
📖 核心知识
1. 订单状态机(核心)
- 主状态:待支付 → 已支付 → 待发货 → 已发货 → 已完成;分支:已取消、退款中;
- 所有状态变更必须带条件更新(
UPDATE ... WHERE status = 前置状态)+ 状态机校验,拒绝非法跳转;状态变更流水表完整留痕。
2. 数据模型设计
- 主单/子单拆分:一次下单多商家/多仓拆为多个子单,各自履约;
- 订单快照:商品名称/价格/优惠在下单时快照进订单,后续商品改价不影响历史订单(避免依赖商品表);
- 垂直拆表:订单主表 + 订单明细 + 支付单 + 物流单,避免大宽表。
3. 性能与存储
- 分库分表:按买家 ID 分片(C 端查询主场景);卖家维度查询同步到 ES 异构索引;
- 冷热分离:三个月内热单在在线库,历史单归档到历史库/数仓,在线库保持小体量;
- 写优化:下单链路同步只做最少动作(建单 + 预占库存),支付回调、通知、积分全部异步化。
4. 一致性与兜底
- 幂等:下单幂等键、支付回调幂等(见《功能设计面试》『如何避免用户重复下单』『如何解决订单重复支付情况』);
- 与库存/优惠/支付的状态一致性靠事件驱动 + 对账修复,不追求分布式事务强一致;
- 超时未支付自动取消:延迟消息 + 扫表兜底(见《功能设计面试》『如何实现一个订单超时取消功能?』)。
🔬 扩展知识
详情
- 【L3】快照的代价量化:订单快照避免历史单受商品改价影响,代价是存储冗余(快照字段约占单量 30%)与下单时多一次商品查询——相比改价引发的资损与客诉,这是必须付的成本。
- 【L3】写路径优化:下单同步路径只做建单 + 预占库存,RT 控制在 50ms 内;支付回调、通知、积分、风控全部异步化,DB 单实例写入约 5000 TPS 是常见瓶颈,大促前必须压测 + MQ 削峰。
- 【L4】分片键权衡:按买家 ID 分片对 C 端查询友好但卖家查单困难,需 ES 异构索引(引入同步延迟与运维成本);备选方案有基因法分片(把卖家信息编入订单号,牺牲买家维度灵活性)与数据同步到独立卖家库(链路复杂),主流仍是 ES 异构索引,核心是接受秒级延迟并做对账。
- 【L4】失效场景:ES 异构索引同步延迟时卖家看到旧数据;大促建单洪峰下 DB 写入瓶颈,需提前压测 + MQ 削峰 + 预案降级。
🏭 实战场景
详情
电商平台大促:峰值 5 万 TPS 下单,订单日增 2000 万笔,买家查单占 90% 流量,卖家后台需按店铺/时间多维查询。
设计要点:下单链路同步只做建单 + 预占库存,支付回调/通知/积分全部异步化,同步路径 RT 控制在 50ms 内;订单按买家 ID 分库分表(32 库 32 表起步,预留扩容),买家查单直查分片;binlog 经 Canal 同步到 ES 支撑卖家多维查询,容忍秒级延迟;下单时快照商品名/价格/优惠,历史单不受商品变更影响;三个月以上订单归档历史库,在线库保持轻量。
生产事故教训:曾有线上事故——订单未存快照直接关联商品表,商品改价后历史订单金额展示错乱,引发大量客诉。快照是订单系统的底线设计。
⚠️ 常见误区
详情
常见误区:
- ❌ "订单展示直接查商品表就行,不用存快照" → 商品改价、下架会让历史订单金额与名称错乱,快照是底线设计。
- ❌ "订单状态流转用应用层 if-else 控制就行" → 并发与多入口(回调、定时任务、人工客服)下必然遗漏分支,必须带条件更新 + 合法迁移表,非法跳转直接拒绝并告警。
- ❌ "用分布式事务保证订单、支付、库存强一致" → 高并发下强一致的性能代价不可承受,业界主流是事件驱动 + 对账修复的最终一致性。
🔀 发散问题
- Q:订单状态机如何防止并发下非法流转? → 更新语句带前置状态条件(
WHERE status = 前置态)+ 影响行数判断,并发下只有一个变更能成功;状态机定义合法迁移表,非法跳转直接拒绝并告警,不依赖应用层 if-else 判断。 - Q:卖家维度查询除了 ES 还有什么方案?各自代价? → 异构索引(ES/双库双写,延迟与一致性成本)、基因法分片(把卖家信息编入订单号,牺牲买家维度灵活性)、数据同步到独立卖家库(链路复杂)。主流是 ES 异构索引,核心是接受秒级延迟并做对账。
- Q:订单表多大需要分库分表?分片后如何支持多维查询? → 单表超 5000 万行或单库写入超几千 TPS 时考虑;分片键选最高频查询维度(买家 ID),其余维度(卖家、手机号、时间范围)靠异构索引,不要在分库表上做全表扫。
- Q:订单幂等和支付回调幂等的共性是什么? → 都是业务唯一键(下单幂等键、渠道交易号 + 支付单号)+ 唯一索引去重 + 状态机条件更新,见本文档「如何设计一个支付系统?」。
【困难】如何设计一个营销/优惠券系统?⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / 营销
💎 关键结论
优惠券本质是钱,设计核心是资产安全 + 高并发发放 + 规则正确:发放用 Redis Lua 预算前置扣减防超发,计算用分(long)+ 分摊算法保证实付不为负,状态变更全程记流水,每日对账保证账实相符。
⚡记忆卡片
- 口诀:发放先预扣、计算用分、分摊归尾、对账保平
- 关键词:预算前置 / Redis Lua / 分(long)/ 分摊尾差 / 唯一索引幂等
- 链路:创建模板 → Redis 预扣发放 → MQ 异步落库 → 下单锁定 → 支付核销 → 对账修复
📖 核心知识
优惠券本质是钱,设计核心是资产安全 + 高并发发放 + 规则正确。
1. 生命周期与状态机
创建(活动模板)→ 发放 → 领取 → 锁定(下单占用)→ 使用/解锁(取消退回)→ 过期;每次状态变更写流水表,可审计对账。
2. 高并发发放(防超发)
- 预算前置:活动总预算预扣到 Redis,Lua 脚本原子扣减(
if remain > 0 then decr),扣成功才写领取记录; - 领取限流:单用户限领 N 张(Redis 计数);风控前置(设备指纹/黑名单防羊毛党);
- 异步落库:领取事件入 MQ 批量入库,Redis 与 DB 定时对账。
3. 优惠计算引擎(最容易出错)
- 优惠层级:商品级(直降)→ 订单级(满减)→ 支付级(立减),层级内互斥/叠加规则配置化;
- 金额计算红线:用分(long)计算不用浮点;分摊算法保证"各商品分摊之和 = 总优惠"(尾差归到最后一件);实付永不为负;
- 规则引擎化:互斥组、优先级、适用商品圈(类目/商品白名单)配置化,避免硬编码。
4. 资产安全
- 使用/退回必须幂等(订单 ID + 券 ID 唯一索引);退款时券是否退回按业务规则配置;
- 每日对账:发放量 = 领取量 + 过期量 + 使用量,不平则告警人工介入。
🔬 扩展知识
详情
- 【L3】发放性能量化:Redis Lua 预扣性能最高(单实例 8~10 万 QPS)但需与 DB 对账保一致性;纯 DB 领取(唯一索引防重)无一致性问题但峰值几千 QPS 就是瓶颈——大促领券必须预算前置到 Redis。
- 【L3】规则引擎设计:互斥组、优先级、适用商品圈全部配置化而非硬编码;规则变更走审批 + 灰度发布,避免运营误配置直接放大成资损。
- 【L4】失效场景:Redis 预算与 DB 记录不一致时(如异步落库丢消息),需对账任务修正;退款时券是否退还需按活动规则配置,默认不退易引客诉。
- 【L4】资损防线设计:"实付不为负 + 优惠总额不超商品价"必须是代码级硬校验,不能依赖运营配置正确——配置只能决定优惠怎么组合,资金红线必须由代码兜底。
🏭 实战场景
详情
618 大促:平台券 1000 万张,开场 10 秒内领取峰值 8 万 QPS,单用户限领 3 张;券可与商品直降叠加但不可与店铺券叠加。
设计要点:发放——券模板总预算 + 单用户限额全部前置 Redis,Lua 原子扣减(单实例 8~10 万 QPS 可扛,或拆两个分片),扣减成功发 MQ 异步落领取记录,同步接口 RT < 20ms;核销——下单时券状态置"锁定"(订单 ID 唯一索引防重),支付成功转"已使用",取消退回;优惠计算引擎硬校验实付 ≥ 0 + 互斥组规则,配置变更需审批;每日对账发放 = 领取 + 过期 + 使用,不平告警。
生产事故教训:曾有线上事故——满减与店铺券叠加规则未配置互斥,两者叠加后实付为负,被羊毛党刷单数万元。"实付不为负 + 优惠总额不超商品价"必须是代码级硬校验。
⚠️ 常见误区
详情
常见误区:
- ❌ "优惠券金额用浮点数算,简单直观" → 浮点精度丢失会导致分摊出现分级误差与账不平,必须以"分"为单位用 long 整型计算。
- ❌ "互斥规则靠运营配置正确就行" → 配置错误不可避免,实付不为负、优惠总额不超商品价必须做代码级硬校验兜底。
- ❌ "Redis 扣预算成功就完事,不用和 DB 对账" → 异步落库可能丢消息,预算与实际领取量会不一致,必须定时对账,不平告警人工介入。
🔀 发散问题
- Q:优惠分摊尾差问题怎么解?举例。 → 满 100 减 10,三件商品 33.33/33.33/33.34,按比例分摊每件减 3.33/3.33/3.34,尾差归最后一件;单件退款时按分摊后金额退,保证总账平衡。用分(long)计算避免浮点精度丢失。
- Q:领券峰值 10 万 QPS,如何防止单用户超领? → Redis 原子计数
INCR coupon:{id}:{userId}前置判断(超过限额拒绝),Lua 脚本内合并"预算扣减 + 个人限额判断"保证原子性;异步落库后用 DB 唯一索引(券 ID + 用户 ID)做最后防线。 - Q:优惠计算引擎如何保证规则变更不出错? → 规则配置化 + 变更审批 + 发布前用历史订单回放验证(同样输入对比新旧引擎结果);金额计算模块单测覆盖率要求 90%+,覆盖分摊、互斥、边界(实付为 0/优惠超商品价)用例。
- Q:优惠券防超发和秒杀扣库存有什么异同? → 同为 Redis Lua 原子扣减 + 异步落库 + 对账兜底,优惠券多了单用户限领与互斥组校验;秒杀则多了漏斗式流量过滤,见本文档「如何设计一个秒杀系统?」。
【困难】如何设计一个支付系统?⭐⭐⭐⭐
🎯 目标等级:L3-L4 | ⏱ 建议用时:20 min | 🏷 标签:系统设计 / 支付
💎 关键结论
支付系统第一原则是资金安全大于一切,宁可慢不可错。核心三模块:支付网关屏蔽渠道差异、支付单与业务订单解耦、账务复式记账;可靠性靠回调幂等 + 主动查单 + 日终对账三层兜底;金额全链路用分(long),余额只允许通过流水变更。
⚡记忆卡片
- 口诀:网关屏蔽、支付单解耦、复式记账、三层兜底
- 关键词:支付网关 / 支付单 / 复式记账 / 回调幂等 / 对账
- 链路:支付请求 → 网关路由渠道 → 支付单状态机 → 回调/查单确认 → 账务记账 → 对账核验
📖 核心知识
支付系统的第一原则:资金安全大于一切,宁可慢不可错。
1. 核心模块
- 支付网关:统一对接微信/支付宝/银联等三方渠道,屏蔽差异(协议、回调格式、对账文件),支持通道路由与自动切换;
- 支付单:与业务订单解耦(一笔订单可对应多笔支付单:重试、换渠道、拆单);状态机:待支付 → 支付中 → 成功/失败/关闭;
- 账务系统:复式记账(每笔交易借贷平衡),余额变更只允许通过账务流水,禁止直接改余额字段。
2. 关键设计
- 回调幂等与状态驱动:三方回调可能重复/乱序到达,以渠道交易号 + 支付单号幂等去重;只允许"待支付 → 成功"单向迁移,终态后拒绝任何变更;
- 主动查单兜底:回调丢失时定时任务主动调渠道查单接口补状态(注意频率限制);
- 对账体系(资损最后防线):每日拉取渠道账单与本地流水逐笔核对,分差错账/长短款处理;实时对账监控金额突变;
- 金额处理:全链路用分(long);退款金额校验 ≤ 已付金额;手续费/分账规则单独建模;
- 安全:签名验签、防重放(见《安全设计面试》『如何设计一个接口签名验证机制?』);支付回调接口只信任服务端签名,不信任前端传参;密钥托管 KMS。
3. 性能量化
大促支付峰值可达几十万 TPS,核心靠异步化(支付请求入 MQ 串行化处理同一用户)+ 渠道连接池复用 + 降级预案(非核心校验可关闭)。
🔬 扩展知识
详情
- 【L4】主动查单的成本权衡:主动查单是回调不可靠的必要兜底,但渠道查单接口有频率限制(通常每单每分钟几次),需控制扫描频率与优先级——刚发起的订单高频查,久未完成的降频。
- 【L4】复式记账的价值:比单表余额更新复杂得多,但它是账务可审计、可对账的唯一途径;绕过流水直接改余额,一旦并发出错将无法追溯,只能人工逐笔对账修复。
- 【L4】失效场景:渠道侧回调与查单结果不一致时(如回调说成功、查单说处理中),以查单为准并进入差错账人工处理;跨时区/跨渠道对账文件格式差异需适配层统一解析。
🏭 实战场景
详情
电商平台双 11:支付峰值 10 万 TPS,对接微信/支付宝/银联三渠道,要求资损零容忍、渠道故障 1 分钟内可切换。
设计要点:支付请求入 MQ 按用户维度串行化(同一用户顺序处理防并发双付);支付网关维护渠道健康度(成功率/RT 实时统计),渠道故障自动路由切换 + 人工一键切换开关;回调幂等(渠道交易号 + 支付单号唯一索引)+ 主动查单兜底 + T+1 三方对账;账务复式记账保证借贷平衡,日切对账不平即告警;10 万 TPS 下应用层无状态水平扩容,瓶颈在渠道侧配额,需提前与渠道报备扩容。
生产事故教训:曾有线上事故——直接 UPDATE 余额字段绕过账务流水,一次并发下余额错乱且无法追溯,最终人工逐笔对账修复。余额只允许通过流水变更。
⚠️ 常见误区
详情
常见误区:
- ❌ "支付回调可信,收到成功回调直接改单即可" → 回调可能丢失、延迟甚至伪造,必须签名验签 + 幂等去重 + 主动查单 + 日终对账多层兜底。
- ❌ "余额字段直接 UPDATE,比复式记账简单" → 绕过流水的余额无法审计、无法对账,并发下错乱不可追溯;余额只允许通过账务流水变更。
- ❌ "退款按用户申请金额直接退就行" → 必须校验累计退款 ≤ 已付金额(应用层 + DB 带条件更新双层校验),并联动发票、已享优惠的业务规则。
🔀 发散问题
- Q:支付回调丢失或延迟的完整兜底链路? → 三层:回调幂等去重 → 未终态支付单定时主动查单(优先级:刚发起的订单高频查,久未完成的降频)→ 日终对账拉渠道账单逐笔比对,长短款进差错账人工处理。
- Q:用户支付中重复点支付按钮怎么办? → 同一订单已存在"支付中"支付单时,不新建支付单而是返回原支付单(唤起同一渠道交易),渠道侧也以商户单号幂等;超时未完成的支付单自动关闭后才能换渠道重发。
- Q:退款如何保证不超额? → 退款单关联原支付单,累计退款额 ≤ 已付额在应用层 + DB 双层校验(
UPDATE ... WHERE 已退 + 本次 <= 已付带条件更新);部分退款需校验与已开发票、已享优惠的联动规则。 - Q:支付幂等和订单幂等的设计共性是什么? → 都是业务唯一键 + 唯一索引 + 状态机单向迁移,终态后拒绝任何变更,见本文档「如何设计一个订单系统?」。