🎯 系统设计题怎么答?(答题框架)

系统设计题没有标准答案,面试官看的是你有没有框架、会不会权衡。千万不要一上来就讲细节。

🎯 通用答题顺序(背下来)

  • 需求 → 估算 → 架构图 → 存储设计 → 核心流程 → 瓶颈与优化 → 权衡
  • 任何一步被追问,都回到「量级」和「权衡」这两个词上

⚡ 场景一:秒杀系统怎么设计?

核心矛盾:瞬时几十万请求抢几百件库存。目标不是「让所有人买到」,而是别把系统打垮 + 不超卖。

Redis + Lua 原子扣库存(防超卖的关键)
-- 返回 1 表示扣减成功,0 表示库存不足,-1 表示重复下单
local stock = redis.call('GET', KEYS[1])
if not stock then return -1 -- 库存没预热
if tonumber(stock) <= 0 then return 0 end
-- 用 setnx 记录该用户已下单,防止重复
if redis.call('SETNX', KEYS[2], 1) == 0 then return -1 end
redis.call('DECR', KEYS[1])
return 1

🎯 面试官爱追问

  • 怎么防超卖?——Redis 原子扣减(Lua 保证「查+扣」原子)+ DB 层 WHERE count > 0 兜底,双保险
  • Redis 扣了但下单失败怎么办?——MQ 消费失败重试;最终失败要回补库存(另发一条补偿消息)
  • 为什么不用数据库行锁扛?——行锁会把并发压到 DB 上,几千 QPS 就打满了;Redis 单机可扛十万级
  • 怎么防止超卖又保证不少卖?——超卖必须杜绝(唯一约束/条件更新);少卖可以靠「超时未支付回补库存」补救

🔗 场景二:短链系统怎么设计?

方案做法优缺点
哈希取模MD5/MurmurHash 长链 → 取前 N 位简单,但有冲突,要额外查重
自增 ID + 进制转换DB/Redis 自增 ID → 62 进制(0-9a-zA-Z)无冲突、短码短;但 ID 连续可被遍历爬取(可加随机扰动)
雪花算法 + 62 进制分布式 ID 转 62 进制分布式友好,长度略长

🎯 面试官爱追问

  • QPS 估算?——假设每天 1 亿次点击,日均约 1160 QPS,峰值按 5 倍约 6000 QPS,单机 Redis 完全够,主要压力在带宽
  • 同一个长链要不要复用短码?——可以(用长链的 hash 查重),但会让不同用户共享统计口径,看业务需求
  • 数据量太大怎么办?——短码做分片键分库分表;冷数据归档;过期短链定时清理

🏆 场景三:实时排行榜怎么做?

常用命令组合
# 增加分数(用户 1001 加 10 分)
ZINCRBY rank:daily 10 user:1001

# 取今日 Top 10(带分数)
ZREVRANGE rank:daily 0 9 WITHSCORES

# 查某人的排名(从 0 开始,所以要 +1)
ZREVRANK rank:daily user:1001

# 取分数区间的人数(如 100 分以上有多少人)
ZCOUNT rank:daily 100 +inf

⏰ 场景四:订单超时未支付自动关闭怎么做?

这是延迟任务的经典题,四种方案各有取舍:

方案做法优点缺点
定时任务轮询每分钟扫一次「未支付且超 30 分钟」的订单实现最简单扫全表压力大、精度差(最多延迟 1 分钟)
JDK DelayQueue本地延迟队列精度高、无外部依赖单机内存、重启丢失,不能分布式
RocketMQ 延迟消息下单时发一条 30 分钟延迟消息可靠、解耦、天然分布式只支持固定延迟级别(4.x 18 级;5.x 支持任意精度)
Redis 过期 + 监听key 设 TTL,监听过期事件精度高Redis 过期事件不保证送达(惰性+定期删除),可能丢
时间轮Netty/Kafka 的时间轮高性能要自己实现,复杂度高

🔢 场景五:分布式 ID 怎么生成?

方案原理适用
UUID随机生成 128 位本地生成、无依赖;但无序,做 MySQL 主键会导致页分裂
数据库自增单表/多表设置不同步长简单;但依赖 DB,性能与可用性受限
Redis INCR原子自增性能好;但依赖 Redis,重启要考虑持久化
雪花算法1 符号 + 41 时间戳 + 5 数据中心 + 5 机器 + 12 序列主流:本地生成、趋势递增、每毫秒 4096 个
号段模式每次从 DB 取一批 ID 缓存到内存(Leaf-segment)对 DB 压力小;ID 连续可被推算
雪花算法的位分配(64 bit)
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
符号位(1) |            时间戳(41 bit, 毫秒)            | 机房(5) | 机器(5) | 序列(12)

// 41 位毫秒时间戳 ≈ 69 年
// 每毫秒最多 2^12 = 4096 个 ID → 单机约 409 万 ID/秒
// 趋势递增 → 对 MySQL 聚簇索引友好

🎯 时钟回拨的三种处理

  • 回拨很小(< 几毫秒):等待追平再生成
  • 回拨较大:直接抛异常,让上层重试(宁可失败也不能生成重复 ID)
  • 彻底方案:备用 workerId(回拨时切换到另一个 workerId 继续生成)

🚦 场景六:限流与降级怎么做?

限流控制请求速率,熔断应对下游故障,降级是主动砍非核心功能。三者常一起出现。

算法思路特点
固定窗口每分钟计数,到点清零简单;临界问题:窗口交界处可能放行 2 倍流量
滑动窗口把窗口切成小格滚动统计平滑;Sentinel 用它(LeapArray)
漏桶请求先入桶,匀速流出严格削峰,不允许突发
令牌桶按速率往桶里放令牌,有令牌才放行允许突发(桶可攒令牌),最常用
单机限流:Guava RateLimiter
// 每秒放 100 个令牌(令牌桶)
RateLimiter limiter = RateLimiter.create(100.0);

if (limiter.tryAcquire()) {
    // 放行
} else {
    // 限流:返回 429 或走降级逻辑
}
分布式限流:Redis + Lua(令牌桶)
-- KEYS[1]=桶 key  ARGV[1]=速率  ARGV[2]=容量  ARGV[3]=当前毫秒时间戳
local rate, cap, now = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1]) or cap
local ts = tonumber(bucket[2]) or now
-- 按流逝时间补充令牌,上限为容量
tokens = math.min(cap, tokens + (now - ts) / 1000 * rate)
local allowed = 0
if tokens >= 1 then tokens = tokens - 1; allowed = 1 end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate * 1000))
return allowed

🎯 面试官爱追问

  • 令牌桶和漏桶的区别?——漏桶匀速流出(严格削峰,无突发);令牌桶按速率放令牌、桶可以攒(允许突发)。秒杀更适合令牌桶
  • 限流放哪里?——网关层做全局兜底(Sentinel/Gateway),应用层做接口级精细控制(@SentinelResource),两层配合
  • 熔断三态?——关闭(正常)→ 打开(快速失败,不请求下游)→ 半开(放少量请求试探,成功则关闭)
  • 降级怎么做?——返回兜底数据(缓存/默认值)、关闭非核心功能(推荐位、评论)、异步化(先落队列后处理)。降级要有开关,能人工干预

🎤 系统设计通用追问(10 条)

🎯 最后的提醒

  • 系统设计题没有唯一答案,敢说「我选 A,因为……代价是……」就赢了一半
  • 被问到不会的:先复述问题 → 说思路 → 承认边界。别硬编
  • 主动提监控、告警、回滚,这是「工程素养」的体现
⚠️ 本页由人工整理,仍建议以官方文档和实际源码为准。