系统设计-高频面试题(更新中...)
写在前面
🔥我把后端Java面试题做了一个汇总,有兴趣大家可以看看!这里👉
⭐️在反复复习面试题时,我发现不同资料的解释五花八门,容易造成概念混淆。尤其是很多总结性的文章和视频,要么冗长难记,要么过于简略,导致关键知识点含糊不清。
⭐️为了系统梳理知识,我决定撰写一份面试指南,不只是简单汇总,而是融入个人理解,层层拆解复杂概念,构建完整的知识体系。我希望它不仅帮助自己更自信地应对面试,也能为同行提供清晰、实用的参考。
系统设计面试题
B站千亿级点赞系统服务架构设计

思考方向:
明确需求 -> 系统架构 -> 数据存储 -> 高并发 -> 容灾降级 -> 异步处理
首先,点赞系统的核心业务能力主要有几类:
- 用户可以对作品点赞、取消点赞,也支持点踩、取消点踩。
- 用户需要查询点赞状态,比如某个稿件自己有没有点过赞,可以是单个查询也可以是批量查询。
- 作品维度要能查点赞总数、点赞人列表。
- 用户维度要能查自己的点赞列表,以及统计自己作品收到的总点赞数。
明确了这些之后,我会把整个系统拆成五个大的部分:
- 流量入口层:负责多机房流量调度和分配。
- 业务网关层:做统一鉴权、风控、限流,避免刷赞。
- 点赞服务层:对外暴露接口(RPC/HTTP),处理核心逻辑。
- 异步任务层:主要用于异步写入、缓存刷新、消息分发。
- 数据存储层:底层数据存储,包括数据库、缓存、KV、本地缓存。
在数据模型上,我会设计两张核心表:
- 点赞记录表:存用户对作品的操作记录(用户 ID、作品 ID、操作类型、时间),可以按用户 ID、作品 ID 建联合索引。
- 点赞计数表:存某个作品的累计点赞数和点踩数,按作品 ID 建索引。
存储我会分三层:
- DB(比如 TiDB 这种分布式数据库),保证最终持久化和回源能力。
- Redis 缓存,抗住大部分读流量,缓存点赞数、用户点赞列表,Key 一般是业务 ID+作品 ID。
- 本地缓存(LocalCache),针对热点作品进一步加速,避免 Redis 被打爆。利用最小堆算法,在可配置的时间窗口范围内,统计出访问最频繁的缓存Key,并将热Key(Value)按照业务可接受的TTL存储在本地内存中。
高并发和热点的优化上:
- 写优化:写请求走异步,点赞计数批量聚合再写 DB,降低 IO 压力。
- 读优化:典型的 Cache-Aside 模式,配合热点识别,把热点 key 放到本地缓存。
- 热点识别:自动识别热门作品,优先加速。
容灾和降级方面,因为点赞是用户强感知的功能:
- 存储层我会做多级容灾,Redis、DB、KV 互相兜底。
- 跨机房容灾,每个机房都有备份,可以快速切换。
- 降级策略方面,如果存储挂了,接口不会直接报错,而是返回空值,保证体验,等服务恢复后再补写。
一致性这块,点赞这种业务允许小范围不一致:
- 我们会有错误重试机制,关键链路比如点赞记录会无限重试。
- 极少数情况下不同存储间数据不一致,也是可以接受的。
最后,吞吐量优化上,可以通过消息队列,把同步写转成异步写,进一步提升吞吐。关键链路会用事务消息加回查机制,保证用户操作不会丢。
如何设计一个百万人抽奖系统?

其实对于商品秒杀、抽奖活动、抢红包类的系统而言,架构设计的思路很多都是类似的,核心思路都是对于这种瞬时超高流量的系统,尽可能在负载均衡层就把99%的无效流量拦截掉。
然后在1%的流量进入核心业务服务后,此时每秒并发还是可能会上万,那么可以基于Redis实现核心业务逻辑 ,抗住上万并发。
最后对于类似秒杀商品发货、抽奖商品发货、红包资金转账之类的非常耗时的操作,完全可以基于MQ来限流削峰,后台有一个服务慢慢执行即可。
V1:负载均衡(分流)
当用户上升到百万的时候,我们就要加多台机器组成集群,用负载均衡把流量分散开。Nginx 都可以做,避免单台服务器过载。但是单纯加机器没用,因为瞬时高峰流量可能是平时的几十倍,所以我们还得继续。
V2:服务限流(防止重复抽奖)
限流的目的是防止瞬时流量把后端打挂。常见策略:
- 用户级限流:比如一个用户一分钟只能抽一次,恶意脚本直接拦掉。
- 可以在网关层用 Sentinel、或者直接在 Nginx 上做 IP 频率限制。
这样就避免了无效请求消耗资源。
V3:共享状态
其实像秒杀、抽奖、抢红包这类场景,都有一个共同点:奖品是有限的,可能 50 万人涌进来,前几百或者前几千请求就把奖品发完了,后续几十万请求都是无效的。如果还让这些请求继续打到后台服务上去执行业务逻辑,纯属浪费资源。
所以更合理的做法是:当奖品一旦抽完,就直接在 负载均衡层 把流量拦掉,返回“抽奖结束”。这样比如 50 万人同时请求,可能只有 2 万请求真正落到后台抽奖服务,剩下 48 万直接在入口层被挡住了,系统压力会小很多。
那这里就涉及一个问题:负载均衡层怎么知道奖品已经发完了?
答案就是要有一个 共享状态。常见的方式有两种:
- 用 Redis 存库存和抽奖状态,轻量级且能抗很高并发,抽奖服务更新状态后,负载均衡层查询即可。
- 或者用 ZooKeeper 这类分布式协调组件,抽奖服务更新一个 znode 节点状态,负载均衡层通过 zk 客户端监听节点变化,立刻感知到奖品发完了。
一般来说,Redis 足够轻量、延迟更低,所以更多时候会首选 Redis。
V4:线程优化
在服务端,还需要调整线程池大小,不能太大也不能太小,需要通过压测找到一个平衡点。经验值可能是 200~500 之间,这样既能充分利用 CPU,又不会让线程上下文切换成本过高。
V5:抽奖逻辑
如果基于MySQL来实现核心的抽奖业务逻辑,抽奖服务频繁对MySQL进行增删改查,这一个MySQL实例也是很难抗住的。通常这种场景下,都是基于Redis来实现核心的业务逻辑。Redis 的 set 或者 list 结构很适合用来随机抽取中奖人,并且可以做到去重和弹出。这样性能很高,MySQL 完全不用顶住高并发。
V6:流量削峰
最后就是中奖通知。假设有一万人中奖,如果直接同步去调通知服务或者写 MySQL,瞬间压力很大。
这时候我们就把中奖结果写到 MQ(Kafka / RocketMQ / RabbitMQ)里,异步处理。通知服务慢慢消费,比如两个实例每秒发 100 条,1 万条也就延迟 1~2 分钟。用户体验基本没影响,但系统压力小很多。
如何从零开始设计一个秒杀系统

-
用户点击秒杀请求(HTTP/HTTPS):请求先到 CDN/WAF(防刷、限速、静态化)。
-
到网关:鉴权(登录、签名) + 幂等检查 + 人机校验(必要时)。
-
本地快速判断(缓存):检查活动是否开始/结束、是否在白名单、每用户限购数、是否已购买等(这些在 Redis /本地缓存)。
-
Token 预占 / 本地速率控制:每台应用机器都在本地维护一个令牌桶,这样避免所有请求都打到 Redis。每个请求先尝试拿一个令牌,拿到才允许继续处理;没拿到就直接返回“繁忙”。
如果想实现全局限流,QPS限制10w内,那就必须有一个全局统一的计数器,否则多台机器加起来会超标。Redis 就可以充当 全局令牌桶 的存储,定时往桶里投放令牌(比如每秒 10 万个)。
-
Redis Lua 脚本做原子库存扣减:核心用 Redis 的 Lua 脚本做原子库存检查与扣减,且同时用 SETNX 控制单用户限购,避免超卖。
-
如果 Redis 扣减成功:立即返回“排队成功/下单中”给用户(低延迟体验),并把用户下单事件发到 MQ。
-
订单服务端 从 MQ 消费,做幂等检查、创建订单(DB 写),冻结/扣款流程、写支付/结算流程。若下单失败,触发补偿(返库存)或人工干预。
-
定期/实时对账:把 Redis 预占与落库的订单做对账(定时任务),若不一致触发补库存或差错处理。
库存 Key
seckill:stock:{goodsId}→ 剩余库存数。- 扣减库存用 Lua 脚本原子操作:
if stock > 0 then decr stock end。用户限购 Key
seckill:user:{activityId}:{userId}→ 用户是否已买/买了多少。- 用
SETNX/INCR保证原子性,避免超购。本地令牌桶 Key
seckill:token:{activityId}→ 存放令牌数量,Redis + 本地桶配合削峰。- 也可以用
list/stream做发放。
为什么不直接写库? -> 会成为瓶颈,容易死锁,异步 + MQ 能削峰并提高可用性。
如何保证最终一致性? -> Redis 记录 + MQ 消费幂等 + 周期性对账 + 补偿流程。
如何防刷? -> CDN + 验证码 + 设备指纹 + 行为风控 + 动态签名。
