后端系统设计速查:从单机到千万级的关键决策
用法:先定位场景(第 8 节对照表)→ 回到对应章节看权衡与坑。所有数字为数量级参考,面试/评审时报数量级即可,不要报精确值。
1. 估算能力(Back-of-envelope)
1.1 必背常数
| 常数 | 值 | 记法 |
|---|
| 一天秒数 | 86,400 ≈ 10^5 | 估算统一按 10^5 |
| 一月秒数 | ≈ 2.6 × 10^6 | — |
| 1 亿请求/天 | ≈ 1,150 QPS(均值) | 「亿/天 ≈ 千级 QPS」 |
| 峰值系数 | 均值 × 2~5(常规);秒杀/热点 × 10+ | 面试默认 ×3 |
| 2^10 | ≈ 10^3(K);2^20 ≈ 10^6(M);2^30 ≈ 10^9(G) | 换算桥梁 |
| 1 字符 | 1B(ASCII)/ 3B(UTF-8 中文) | — |
| long/时间戳 | 8B;UUID 16B;MD5 16B | — |
1.2 必背延迟数字(数量级)
| 操作 | 延迟 | 对比感受 |
|---|
| L1 缓存访问 | ~1 ns | 基准 |
| 内存随机访问 | ~100 ns | — |
| 内存顺序读 1MB | ~50 μs | 顺序 >> 随机 |
| SSD 随机读 | ~100 μs | 比内存慢 10^3 |
| SSD 顺序读 1MB | ~1 ms | — |
| 机械盘寻道 | ~10 ms | 避免随机 IO 的根源 |
| 同机房 RTT | ~0.5 ms | 一次 RPC 的底价 |
| 同城跨机房 RTT | ~1~3 ms | 同城双活可行的原因 |
| 跨国 RTT | ~150 ms | 异地多活难做强一致的原因 |
| Redis GET | ~0.1~1 ms(含网络) | — |
| MySQL 索引点查 | ~1~10 ms | — |
1.3 单机容量参考(数量级,用于判断「要不要拆」)
| 组件 | 单机能力(参考) |
|---|
| Nginx | 数万~十万 QPS(转发) |
| Redis 单实例 | ~10 万 QPS |
| MySQL 单实例 | 读 ~5k QPS / 写 ~1~2k TPS;单表建议 < 千万~5000 万行 |
| Kafka 单 broker | 数十万 msg/s(顺序写 + 零拷贝) |
| 普通 Java/Go 服务 | 数百~数千 QPS/实例(取决于业务逻辑) |
| 单机连接数 | 数万长连接常规,百万需专门调优 |
1.4 估算套路(固定四步)
- QPS:DAU × 人均请求数 ÷ 10^5 → 均值;×3 → 峰值;读写比(常见 10:1~100:1)拆分读写。
- 存储:单条大小 × 日增条数 × 保留天数 × 副本数(通常 ×3)。
- 带宽:峰值 QPS × 平均响应体大小。1 万 QPS × 100KB = 1GB/s ≈ 8Gbps → 必须上 CDN。
- 机器数:峰值 QPS ÷ 单机能力 × 冗余系数(1.5~2,容忍 N+1 故障)。
坑:只算均值不算峰值;忘算副本和索引膨胀(MySQL 索引可占数据量 50%+);忘了流量增长(按 1~2 年 ×2~5 预留)。
2. 缓存
2.1 缓存模式选型
| 模式 | 读写路径 | 何时用 | 代价/坑 |
|---|
| Cache-Aside(旁路,默认选它) | 读:查缓存→miss 查库→回填;写:先更新 DB,再删缓存 | 绝大多数读多写少场景 | 需自己处理一致性;删除失败要重试 |
| Read/Write-Through | 缓存层代理读写 DB | 有统一缓存中间件时 | 实现复杂,业务侵入小但基建要求高 |
| Write-Behind(异步写回) | 先写缓存,异步批量落库 | 写极高频、可容忍丢失(点赞数、计数器) | 宕机丢数据;顺序难保证 |
| 本地缓存(Caffeine/Guava) | 进程内 | 热点小数据、配置类;作为 Redis 前的 L1 | 多实例不一致;靠短 TTL + 广播失效缓解 |
2.2 三大经典问题
| 问题 | 定义 | 对策(按优先级) |
|---|
| 穿透 | 查不存在的 key,永远打到 DB | ① 缓存空值(短 TTL 1~5min)② 布隆过滤器拦截 ③ 参数合法性校验(负数 ID 直接拒) |
| 击穿 | 单个热点 key 过期瞬间,并发全打 DB | ① 互斥锁/singleflight:只放一个请求回源 ② 热点 key 逻辑过期(不设 TTL,异步刷新)③ 热点永不过期 + 主动更新 |
| 雪崩 | 大量 key 同时过期或 Redis 整体挂掉 | ① TTL 加随机抖动(base + rand(0, 10%·base))② Redis 高可用(哨兵/集群)③ 服务侧限流 + 降级兜底 ④ 多级缓存 |
2.3 缓存一致性(面试高频)
- 默认答案:Cache-Aside + 先更 DB 后删缓存 + 删除失败重试(消息队列/订阅 binlog)。
- 为什么删不是更:更新缓存在并发下更容易产生旧值覆盖;删除让下次读回填,天然收敛。
- 为什么先 DB 后删:反过来(先删后写 DB)在「删除后、DB 写完前」被并发读回填旧值,脏窗口更大。
- 极端并发下「读 miss 回填旧值 + 写删缓存」交错仍可能不一致 → 延迟双删(写后延迟几百 ms 再删一次)或 订阅 binlog(Canal)异步删 兜底。
- 结论:缓存只能做到最终一致。要强一致的数据(余额、库存扣减的判定值)不要依赖缓存做决策,直接走 DB/加锁。
其他坑:大 key(>10KB 就要警惕,压缩/拆分);热 key(本地缓存 + 分片打散 key_{1..N});缓存与 DB 字段演进不同步(值里带版本号)。
3. 数据库
3.1 SQL vs NoSQL 选型
| 维度 | 选 SQL(MySQL/PG) | 选 NoSQL |
|---|
| 数据关系 | 多表关联、事务、强 schema | 弱关系、schema 多变 |
| 一致性 | 需要 ACID(订单、支付、账务) | 可接受最终一致 |
| 查询模式 | 灵活 ad-hoc 查询 | 查询模式固定、按 key 访问 |
| 量级 | 单表千万级内舒适 | 天然水平扩展 |
| NoSQL 类型 | 代表 | 典型场景 |
|---|
| KV | Redis | 缓存、会话、计数、排行榜(zset)、分布式锁 |
| 文档 | MongoDB | 内容/商品详情、schema 多变、嵌套结构 |
| 宽列 | HBase/Cassandra | 海量写入、时序类、消息存储、画像 |
| 搜索 | Elasticsearch | 全文检索、多维筛选聚合(不当主存储) |
| 图 | Neo4j | 社交关系、风控关联分析 |
务实原则:默认 MySQL;专门问题上专门组件(搜索→ES,缓存→Redis);NoSQL 是补充不是替代,核心账务数据留在关系库。
3.2 索引
| 要点 | 说明 |
|---|
| 最左前缀 | 联合索引 (a,b,c) 支持 a / a,b / a,b,c;跳过 a 不走索引 |
| 覆盖索引 | 查询列全在索引里 → 免回表,高频查询优先设计 |
| 失效场景 | 索引列上用函数/运算、隐式类型转换(字符串列传数字)、前导 %like%、OR 混用无索引列 |
| 区分度 | 低区分度列(性别、状态)单独建索引无意义;可放联合索引尾部 |
| 代价 | 每个索引拖慢写入 + 占存储;单表索引建议 ≤ 5~6 个 |
| 主键 | 用自增/趋势递增(雪花 ID);随机 UUID 导致页分裂、写放大 |
坑:explain 看 type(至少 range)和 rows;深分页 limit 100000,10 要改游标(where id > ?)或延迟关联。
3.3 扩展路径(按顺序,别跳步)
参数/索引/SQL 优化 → 读写分离 → 垂直拆分(按业务拆库) → 水平分库分表(最后手段)
| 手段 | 何时用 | 代价 |
|---|
| 读写分离 | 读远大于写、读扛不住 | 主从延迟:写后立即读可能读到旧值 → 关键读走主库 / 会话粘性 / 延迟检测 |
| 垂直分库 | 业务边界清晰、单库容量/连接数瓶颈 | 跨库无 join、无本地事务 |
| 水平分表 | 单表 > 千万~5000 万行、单库写入瓶颈 | 见下 |
分库分表关键决策:
| 决策点 | 选项与建议 |
|---|
| 分片键 | 选最高频查询维度(C 端通常 user_id);一旦选定极难改 |
| 算法 | 哈希取模(分布均匀,扩容需迁移→用一致性哈希/预分片如 1024 片缓解);范围/时间(利于冷热分离,但有热点尾部) |
| 非分片键查询 | ① 基因法(把 userid 片段编入 orderid)② 异构索引表/ES ③ 广播查询(尽量避免) |
| 全局 ID | 雪花算法(注意时钟回拨)/ 号段模式(Leaf) |
| 跨片问题 | join 改应用层聚合;分页用 ES/汇总表;事务见第 5 节 |
坑:过早分库分表(复杂度巨大,先确认优化空间用尽);分片键选错导致大量跨片查询;扩容方案没提前设计。
3.4 主从复制
| 模式 | 语义 | 取舍 |
|---|
| 异步复制 | 主库提交即返回 | 最快;主库挂 → 丢最近事务 |
| 半同步 | 至少 1 个从库 ack | 平衡(默认推荐);超时会退化为异步 |
| 全同步/组复制(MGR) | 多数派确认 | 不丢数据;延迟高、吞吐降 |
主从延迟来源:大事务、从库单线程回放(开并行复制)、从库机器差。处理写后读:强制读主 / GTID 等待 / 客户端缓存刚写的值。
4. 消息队列
4.1 什么时候引入(三大理由,说不出理由就别加)
| 目的 | 场景 | 例子 |
|---|
| 异步 | 非核心步骤移出同步链路,降 RT | 下单后发短信/积分/推荐 |
| 解耦 | 上游不关心下游是谁、有几个 | 订单事件被 N 个系统消费 |
| 削峰 | 瞬时流量 >> 下游处理能力 | 秒杀请求排队慢慢消化 |
引入的代价:系统复杂度 +1(可用性依赖、消息丢失/重复/积压三大新问题)、链路变最终一致、排查跨系统。同步链路上强依赖结果的调用不要改 MQ。
4.2 选型速览
| MQ | 优势 | 适用 |
|---|
| Kafka | 吞吐最高、分区顺序、生态(流处理) | 日志/埋点、大数据管道、事件流 |
| RocketMQ | 事务消息、延迟消息、业务特性全 | 电商/交易类业务解耦 |
| RabbitMQ | 路由灵活、低延迟 | 中小规模业务、复杂路由 |
| Pulsar | 存算分离、多租户 | 云原生、超大规模多业务线 |
4.3 三大必答问题
① 消息不丢(三段各自保证)
| 环节 | 手段 |
|---|
| 生产端 | 同步发送 + 重试;或本地消息表(见 5.3) |
| Broker | 刷盘策略 + 多副本(Kafka acks=all + min.insync.replicas>=2) |
| 消费端 | 处理成功后再提交 offset / ack(禁止先 ack 后处理) |
② 重复消费(一定会发生,别想「恰好一次」)
- 事实:投递语义务实上只有 at-least-once + 消费端幂等 = 业务上的 exactly-once。
- 幂等手段:唯一键约束(消息 ID/业务单号入去重表)、Redis setnx 去重(注意 TTL 与原子性)、状态机(只允许 pending→paid,重复消息推不动状态)、乐观锁版本号。
③ 顺序性
- 全局顺序代价极高(单分区单消费者),绝大多数场景只需要分区顺序:同一业务键(同一订单/用户)hash 到同一分区 + 分区内单线程消费。
- 坑:消费者内部再开线程池就破坏顺序;扩分区会打乱既有 key 的路由。
④ 积压处理:先扩消费者(≤ 分区数才有效)→ 消费逻辑批量化/去掉慢操作 → 紧急时新建大分区数 topic 转发分流 → 事后补:消费限速与容量预估。
坑:死信队列必须有监控与人工处理流程;消息体别放大对象(放 ID,消费端反查)。
5. 一致性与可用性
5.1 CAP 实战理解
- P(分区容忍)在分布式下不可放弃,真实选择是 分区发生时选 C 还是 A;无分区时权衡的是延迟 vs 一致性(PACELC)。
- 落地:注册中心选 AP(Eureka/Nacos-AP,服务发现宁可返回旧列表);分布式协调选 CP(etcd/ZooKeeper,锁和选主不能错)。
- 同一系统里不同数据可以不同策略:余额强一致,头像昵称最终一致。先问业务「这条数据错 1 秒的代价是什么」。
5.2 最终一致性常见实现
| 手段 | 机制 | 适用 |
|---|
| 可靠消息(本地消息表/事务消息) | 见 5.3 | 跨服务数据同步的默认方案 |
| 订阅 binlog(Canal/Debezium/CDC) | DB 变更 → MQ → 下游 | 缓存失效、异构索引、数仓同步;对业务零侵入 |
| 定时对账 + 补偿 | 周期比对两边数据修复差异 | 一切最终一致方案的兜底,必须有 |
| 重试 + 幂等 | 失败自动重试直到成功 | 所有异步链路的基础件 |
5.3 分布式事务方案对比
| 方案 | 一致性 | 侵入性 | 性能 | 何时用 |
|---|
| 2PC/XA | 强 | 低(DB 层) | 差(同步阻塞、协调者单点) | 传统场景、跨库低并发;互联网基本不用 |
| TCC | 最终(业务层强控制) | 极高(每个操作写 Try/Confirm/Cancel ×3) | 好 | 资金类、需要资源预留(冻结额度) |
| Saga | 最终(可见中间态) | 中(每步写补偿) | 好 | 长流程多步骤(订票=机票+酒店+车) |
| 本地消息表 | 最终 | 中 | 好 | 默认首选:业务 DB 事务内写消息表,后台扫表投递 MQ,消费端幂等 |
| 事务消息(RocketMQ) | 最终 | 低 | 好 | 有 RocketMQ 时替代本地消息表(半消息 + 回查) |
| 最大努力通知 | 弱 | 低 | 好 | 通知外部第三方(回调 + 重试 + 对方主动查询) |
决策树:能拆成单库事务就别上分布式事务 → 能接受最终一致选本地消息表/事务消息 → 资金预留类选 TCC → 长补偿链选 Saga。空回滚、悬挂、幂等是 TCC/Saga 的三个必处理问题。
6. 限流、熔断、降级
6.1 限流算法
| 算法 | 原理 | 优缺点 | 适用 |
|---|
| 固定窗口 | 每窗口计数 | 实现最简;临界突刺(窗口边界 2 倍流量) | 粗粒度够用时 |
| 滑动窗口 | 细分子窗口滑动统计 | 解决突刺;内存略多 | 通用计数限流(Sentinel 默认) |
| 漏桶 | 恒定速率流出 | 绝对平滑;无法应对突发,多余排队/丢弃 | 对下游保护、调用第三方(对方限速) |
| 令牌桶 | 恒速放令牌,可攒 | 允许桶容量内的突发,最常用 | 网关/接口限流默认选它(Guava RateLimiter) |
分布式限流:Redis + Lua(原子性)实现集群级配额;单机限流兜底防 Redis 挂。限流维度:全局 QPS / 用户 / IP / 接口分级。被限流的响应:返回 429 + 友好提示/排队页,别返回 500。
6.2 熔断(保护调用方自己)
- 三态机:关闭 →(错误率/慢调用比例超阈值)→ 打开(直接快速失败)→(冷却期后)→ 半开(放少量探测)→ 成功恢复/失败再打开。
- 关键参数:统计窗口(如 10s)、最小请求数(防小样本误判)、错误率阈值(如 50%)、冷却时间(如 30s)。
- 熔断触发后必须配降级逻辑(fallback),否则只是把错误换了个形式。
- 组件:Sentinel(国内主流)/ Resilience4j。
6.3 降级(弃车保帅)
| 类型 | 例子 |
|---|
| 功能降级 | 大促关闭推荐/评论,保下单支付 |
| 数据降级 | 实时数据不可用 → 返回缓存/静态兜底数据 |
| 写降级 | 同步写改异步写、暂停非核心写入 |
| 读降级 | 强一致读降为读缓存 |
务实要点:降级开关要提前埋好(配置中心一键切换),大促前演练;核心/非核心链路要事先分级,别等出事再决定砍谁。三件套关系:限流防洪(入口),熔断止损(依赖),降级保命(体验)。
7. 高可用与扩展
7.1 无状态化(水平扩展的前提)
- 会话 → Redis / JWT;本地文件 → 对象存储;进程内定时任务 → 分布式调度(XXL-Job)+ 分布式锁防重复执行;本地缓存 → 允许短暂不一致或广播失效。
- 检验标准:任意实例可随时被杀掉/新增,用户无感。做不到就没有弹性伸缩和滚动发布。
7.2 负载均衡
| 层级 | 代表 | 特点 |
|---|
| DNS | 智能 DNS | 地理调度;生效慢(TTL),不适合故障切换 |
| L4 | LVS/云 SLB | 性能极高,按 IP:Port 转发 |
| L7 | Nginx/Envoy | 按 URL/Header 路由、灰度;性能次之 |
| 客户端 | 微服务框架(gRPC/Dubbo) | 服务发现 + 客户端选节点,少一跳 |
| 算法 | 适用 |
|---|
| 轮询/加权轮询 | 节点同构/异构的默认选择 |
| 最少连接 | 请求耗时差异大 |
| 一致性哈希 | 需要亲和性(缓存命中、有状态分片);节点变动只影响 1/N |
| IP Hash | 简单会话粘性(慎用:与无状态化冲突,且 NAT 下倾斜) |
必配:健康检查(主动探测 + 摘除)+ 慢启动(新节点渐进引流)。
7.3 垂直 vs 水平扩展
| 垂直(加配置) | 水平(加机器) |
|---|
| 优点 | 零架构改动、立即见效 | 无上限、天然冗余 |
| 缺点 | 有硬件天花板、单点、成本超线性 | 要求无状态/可分片,架构复杂 |
| 务实 | 早期先垂直,快且便宜 | 有状态组件(DB)垂直扛到不得不拆;无状态服务尽早水平 |
7.4 幂等设计(分布式系统的地基)
重试无处不在(超时重试、MQ 重投、用户重复点击),所有写接口默认要考虑幂等。
| 手段 | 机制 | 适用 |
|---|
| 唯一索引 | 业务单号/请求 ID 唯一约束,重复插入报错即忽略 | 创建类操作(最可靠) |
| Token 机制 | 先取 token,提交时原子校验并删除 | 表单防重复提交 |
| 状态机 | 只允许合法状态迁移,非法迁移拒绝 | 订单/工单类流程 |
| 乐观锁 | update ... set v=v+1 where id=? and v=? | 并发更新 |
| Redis setnx | 请求 ID 去重(设 TTL) | 高性能场景(注意 Redis 挂时的兜底) |
坑:只在网关做幂等不够,重试可能发生在任意内部环节;「查询后插入」不加唯一索引在并发下必然重复。
7.5 可用性数字
| 级别 | 年不可用 | 大致含义 |
|---|
| 99.9% | ~8.8 小时 | 单机房、有基本冗余 |
| 99.99% | ~53 分钟 | 同城多机房、自动故障转移 |
| 99.999% | ~5 分钟 | 异地多活,成本陡增 |
串联链路可用性相乘(0.999^5 ≈ 0.995)→ 依赖越多可用性越低,用异步/缓存/降级切断强依赖。容灾演进:主备(冷/热)→ 同城双活 → 异地多活(按用户分片,单元内闭环,代价极大,非必要不做)。
8. 场景 → 推荐方案速查表
| 场景 | 推荐方案(骨架) | 关键点 |
|---|
| 读多写少(资讯/商品页) | CDN + Redis Cache-Aside + MySQL 读写分离 | 缓存一致性用「先更 DB 后删缓存 + binlog 兜底」 |
| 秒杀/瞬时高并发写 | 前置拦截(按钮置灰/答题)→ 限流 → Redis 预扣库存(Lua 原子)→ MQ 排队 → DB 异步落单 | 库存防超卖:Redis 原子扣减 + DB 乐观锁双保险;幂等 |
| 计数/点赞/浏览量 | Redis incr + 定时/异步批量落库 | 容忍少量丢失;热 key 分片 |
| 排行榜 | Redis zset | 大榜单只存 TopN + 定期归档 |
| Feed 流 | 推模式(写扩散,粉丝少)/ 拉模式(读扩散,大 V)/ 推拉结合(在线推、大 V 拉) | 大 V 是分界点(粉丝 > 万级用拉) |
| 短链系统 | 发号器(号段/雪花)+ 62 进制编码 + Redis 缓存映射 | 301 vs 302(要统计选 302);布隆防穿透 |
| 分布式锁 | Redis SET NX PX + 唯一持有者标识 + Lua 释放;要求可靠用 Redisson(看门狗续期);强一致场景用 etcd/ZK | 锁超时 vs 业务超时;主从切换丢锁风险(RedLock 争议大,关键场景用 CP 存储) |
| 分布式 ID | 雪花算法(注意时钟回拨)或号段模式 | 需趋势递增利于 InnoDB 写入 |
| 延迟任务(订单超时关闭) | RocketMQ 延迟消息 / Redis zset 轮询 / 时间轮 | 兜底:定时扫表补偿 |
| 跨服务数据一致(下单+扣库存+积分) | 本地消息表 / RocketMQ 事务消息 + 消费幂等 + 对账 | 资金冻结类才上 TCC |
| 海量日志/埋点 | Kafka → Flink → ES/ClickHouse/数仓 | 顺序写 + 批量,别用 MySQL 扛 |
| 全文搜索/多维筛选 | MySQL 主存 + binlog 同步 ES | ES 不做主存储,接受秒级延迟 |
| 单表过大(>5000 万) | 先归档冷数据/优化 → 再水平分表(user_id 分片 + 基因法/异构索引解决其他维度查询) | 分片键 = 最高频查询维度 |
| 写后立即读要求高 | 读主库 / GTID 等待 / 客户端回填 | 主从延迟无法根除,只能绕 |
| 依赖的第三方不稳定 | 超时(必设,且 < 上游超时)+ 重试(带退避 + 幂等)+ 熔断 + 降级兜底 | 超时是第一道防线,比熔断更重要 |
| 大促保稳 | 容量压测 → 分级降级预案 → 入口限流 → 全链路监控 + 一键开关 | 预案没演练过 = 没有预案 |
附:面试/评审答题框架
1. 澄清需求与规模(QPS/存储/读写比/一致性要求)—— 用第 1 节估算
2. 先给最简架构(LB + 无状态服务 + DB + 缓存),能跑通
3. 按瓶颈逐步演进:读瓶颈→缓存/读写分离;写瓶颈→MQ/分库分表
4. 每个组件说清「为什么引入 + 引入后的新问题 + 如何兜底」
5. 收尾谈非功能项:幂等、监控告警、降级预案、容量冗余
核心心法:每引入一个组件,都在用复杂度换某个能力——说不出换来了什么,就不要引入。