系统设计面试
系统设计面试
【困难】如何设计一个秒杀系统?⭐⭐⭐⭐⭐
秒杀系统所要应对的场景就是:瞬时海量请求。
秒杀系统的难点
- 高并发:秒杀系统是极致的高并场景发自不用说。其高并发可以细分为二:
- 并发读:主要是读取剩余库存量以及商品信息
- 并发写:主要是下单后,系统写入订单记录
- 超卖:秒杀系统中售卖的商品一般都是性价比很高,不怎么赚钱,甚至赔钱赚哟喝的商品。一旦出现超卖现象,会给商家带来巨大的经济损失。从系统层面来看,比如某秒杀商品本来库存 100 件,但是在高并发场景下,瞬时下单量超过 100 件,处理不当,让这些下单都成功了,就会出现超卖。
- 恶意请求:有些人为了低价购入秒杀商品,通过在多台机器上跑脚本,模拟大量用户抢商品的请求(走自己的路,让别人无路可走)。
- 数据库崩溃:海量请求下,如果没有 MQ 削峰,没有过载保护,让所有请求都打到数据库,那么数据库基本就挂了。数据库如果挂了,也会波及其他业务,从而可能让整个系统、网站陷入瘫痪。
- 对现有业务造成冲击
秒杀系统设计目标
秒杀系统架构的思考角度可以概括为:稳、准、快
- 稳(高可用):系统架构要满足高可用,系统要能撑住活动。
- 准(一致性):商品减库存方式非常关键,不能出现超卖。
- 快(高性能):整个请求链路,从前端到后端,依赖组件都要做到协同优化。

前端优化-静态页面
把秒杀商品页面静态化,减少查数据库的 IO 开销。然后,可以将这些静态页面做 CDN 缓存,如果项目是前后端分离的,还可以在反向代理服务器侧设置静态缓存。
如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。相应的页面可以提前做前端缓存,这样就不需要向后台查询商品信息。
前端优化-按钮控制
在秒杀活动开启时间前,下单按钮禁用。
此外,按钮一旦点击之后,禁用一段时间,防止有人疯狂输出。
后端优化-限流、熔断、降级、隔离
秒杀活动,本质上还是一个营销活动,性质和打折、促销一样。
秒杀系统设计底线原则,是不应该影响现有业务。所以,为了避免防不胜防,百密一疏的情况下,秒杀系统崩了。
- 隔离:将秒杀系统、数据与其他正常业务隔离。彼此隔离,自然互不影响。
- 限流:设置阈值,超过阈值,拒绝请求。防止数据库被打死。
- 降级:保证核心业务继续工作,非核心业务各安天命。
- 熔断:不要影响别的系统。
后端优化-多级缓存
缓存要预热,避免瞬间流量冲击。
此外,防止雪崩、穿透、击穿问题的常规处理要做好。
缓存也要保证高可用。
后端优化-流量削峰
削峰的思路:排队、答题、分层过滤。
- 排队:用消息队列来缓冲瞬时流量的方案。但是,消息队列自身也有上限,如果积压过多,也会处理不了。
- 答题(摇一摇):可以限制秒杀器并延缓请求。
- 分层过滤:采用漏斗式的设计尽可能拦截无效请求。

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