# 后端系统设计速查：从单机到千万级的关键决策

> 用法：先定位场景（第 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 估算套路（固定四步）

1. **QPS**：DAU × 人均请求数 ÷ 10^5 → 均值；×3 → 峰值；读写比（常见 10:1~100:1）拆分读写。
2. **存储**：单条大小 × 日增条数 × 保留天数 × 副本数（通常 ×3）。
3. **带宽**：峰值 QPS × 平均响应体大小。1 万 QPS × 100KB = 1GB/s ≈ 8Gbps → 必须上 CDN。
4. **机器数**：峰值 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 片缓解）；范围/时间（利于冷热分离，但有热点尾部） |
| 非分片键查询 | ① 基因法（把 user_id 片段编入 order_id）② 异构索引表/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. 收尾谈非功能项：幂等、监控告警、降级预案、容量冗余
```

> 核心心法：**每引入一个组件，都在用复杂度换某个能力——说不出换来了什么，就不要引入。**
