系统设计基础
SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP
官方文档:https://learn.microsoft.com/zh-cn/azure/architecture/patterns/
适用版本:2026-05-07 整理
核心指标
| 指标 | 定义 | 典型值参考 |
|---|---|---|
| QPS(每秒查询数) | 每秒处理的请求数量 | 普通 Web:100-1000,高并发:10000+ |
| TPS(每秒事务数) | 每秒完成的事务数量 | 通常低于 QPS(事务含多个操作) |
| 延迟(Latency) | 请求从发出到收到响应的时间 | P99 < 100ms 为良好目标 |
| 吞吐量(Throughput) | 单位时间内处理的数据量 | 与 QPS 相关但不等同 |
| 可用性(Availability) | 系统正常运行时间占比 | SLA 见下表 |
SLA 对照表:
| 可用性 | 年停机时间 | 月停机时间 |
|---|---|---|
| 99%(两个九) | 87.6 小时 | 7.3 小时 |
| 99.9%(三个九) | 8.76 小时 | 43.8 分钟 |
| 99.99%(四个九) | 52.6 分钟 | 4.4 分钟 |
| 99.999%(五个九) | 5.26 分钟 | 26.3 秒 |
水平扩展 vs 垂直扩展
| 维度 | 水平扩展(Scale Out) | 垂直扩展(Scale Up) |
|---|---|---|
| 方式 | 增加机器数量 | 升级单机配置(CPU/内存/磁盘) |
| 上限 | 理论无上限 | 受单机硬件限制 |
| 成本 | 线性增长,可用廉价机器 | 高端硬件价格急剧上升 |
| 复杂度 | 需要处理分布式问题 | 简单,无分布式复杂性 |
| 适用场景 | 无状态服务、读多写少 | 数据库、有状态服务初期 |
| 故障影响 | 单点故障影响小 | 单点故障影响全部流量 |
选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。
缓存策略
缓存模式对比
| 模式 | 流程 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Cache-Aside(旁路缓存) | 读:先缓存→未命中→查 DB→写缓存;写:先更新 DB→删缓存 | 实现简单,按需加载 | 首次访问慢,可能脏读窗口 | 最常用,读多写少 |
| Write-Through(写透) | 写操作同时更新缓存和 DB | 缓存始终与 DB 一致 | 写延迟增加 | 强一致性要求 |
| Write-Behind(写回) | 写操作只写缓存,异步批量刷 DB | 写性能极高 | 数据丢失风险 | 日志、计数器 |
| Refresh-Ahead(预刷新) | 缓存过期前主动刷新 | 避免缓存击穿 | 可能缓存不常用数据 | 热点数据 |
缓存三大问题
缓存穿透(查询不存在的 key,每次都打到 DB):
- 解决:布隆过滤器(Bloom Filter)拦截非法 key;或将空结果也缓存(短 TTL)
缓存击穿(热点 key 过期,瞬间大量请求打到 DB):
- 解决:互斥锁(只让一个请求重建缓存);或热点 key 不设过期时间,主动更新
缓存雪崩(大量 key 同时过期,或缓存服务宕机):
- 解决:TTL 随机抖动(
base_ttl + random(0, 300));缓存服务高可用(主从/集群);熔断降级
import random
import time
def set_cache_with_jitter(key: str, value, base_ttl: int = 3600):
"""加入随机抖动防止缓存雪崩"""
jitter = random.randint(0, base_ttl // 10) # 10% 抖动
ttl = base_ttl + jitter
redis_client.setex(key, ttl, value)
限流算法
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 计数器按固定时间窗口重置 | 实现简单 | 窗口临界突刺问题 | 粗粒度限流 |
| 滑动窗口 | 以当前时间为终点的动态窗口 | 解决临界突刺 | 存储开销较大 | API 限流 |
| 漏桶(Leaky Bucket) | 请求入队,固定速率出队 | 平滑输出,绝对稳定 | 无法应对突发流量 | 消息队列消费 |
| 令牌桶(Token Bucket) | 固定速率生成令牌,有令牌才能通过 | 允许一定突发流量 | 实现稍复杂 | 接口限流(推荐) |
令牌桶 Python 实现:
import time
import threading
class TokenBucket:
def __init__(self, rate: float, capacity: int):
"""
参数:
rate - 每秒生成的令牌数
capacity - 桶的最大容量
"""
self.rate = rate
self.capacity = capacity
self._tokens = capacity
self._last_refill = time.monotonic()
self._lock = threading.Lock()
def consume(self, tokens: int = 1) -> bool:
with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
return False
def _refill(self):
now = time.monotonic()
elapsed = now - self._last_refill
self._tokens = min(self.capacity, self._tokens + elapsed * self.rate)
self._last_refill = now
Redis 实现分布式限流(滑动窗口):
-- KEYS[1]: 限流 key
-- ARGV[1]: 当前时间戳(毫秒)
-- ARGV[2]: 窗口大小(毫秒)
-- ARGV[3]: 限流阈值
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1
end
return 0
API 设计规范
RESTful 设计原则
| 资源操作 | HTTP 方法 | URL 示例 | 成功状态码 |
|---|---|---|---|
| 获取列表 | GET | /api/v1/users |
200 |
| 获取单个 | GET | /api/v1/users/{id} |
200 |
| 创建 | POST | /api/v1/users |
201 |
| 完整更新 | PUT | /api/v1/users/{id} |
200 |
| 部分更新 | PATCH | /api/v1/users/{id} |
200 |
| 删除 | DELETE | /api/v1/users/{id} |
204 |
URL 命名规则:
- 用名词复数,不用动词:
/users而非/getUsers - 层级关系:
/users/{id}/orders - 过滤用 query 参数:
/users?status=active&page=1&page_size=20
版本控制
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径(推荐) | /api/v1/users |
直观,缓存友好 | URL 增长 |
| 请求头 | Accept: application/vnd.api+json;version=1 |
URL 整洁 | 不直观,难以调试 |
| Query 参数 | /api/users?version=1 |
灵活 | 污染查询参数 |
分页方案对比
| 方案 | 实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Offset 分页 | LIMIT 20 OFFSET 200 |
可跳转任意页 | 大偏移性能差,数据漂移 | 数据量小,需要跳页 |
| Cursor 分页 | WHERE id > last_id LIMIT 20 |
性能稳定,无漂移 | 不能跳页 | 大数据量,无限滚动 |
| Keyset 分页 | 多字段游标 | 精确、高性能 | 实现复杂 | 时间线类数据 |
Cursor 分页响应格式:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6IDEwMH0",
"has_next": true
}
}
幂等设计
| HTTP 方法 | 是否天然幂等 | 实现方式 |
|---|---|---|
| GET | 是 | - |
| DELETE | 是(返回 204 或 404) | - |
| PUT | 是 | 完整替换资源 |
| POST | 否 | 客户端传 Idempotency-Key 头,服务端去重 |
| PATCH | 否(视实现而定) | 用绝对值而非相对值 |
# FastAPI 幂等 Key 中间件示意
async def idempotency_middleware(request: Request, call_next):
idempotency_key = request.headers.get("Idempotency-Key")
if idempotency_key and request.method == "POST":
cached = await redis.get(f"idem:{idempotency_key}")
if cached:
return JSONResponse(content=json.loads(cached), status_code=200)
response = await call_next(request)
if idempotency_key and response.status_code in (200, 201):
body = b""
async for chunk in response.body_iterator:
body += chunk
await redis.setex(f"idem:{idempotency_key}", 86400, body)
return Response(content=body, status_code=response.status_code,
headers=dict(response.headers))
return response
分布式 ID
| 方案 | 生成方式 | 长度 | 排序性 | 适用场景 |
|---|---|---|---|---|
| 数据库自增 | DB 序列 | 取决于配置 | 有序 | 单库,简单场景 |
| UUID v4 | 随机生成 | 128 bit / 36 char | 无序 | 不需要排序的 ID |
| ULID | 时间戳 + 随机 | 128 bit / 26 char | 时间有序 | 推荐替代 UUID |
| 雪花算法(Snowflake) | 时间戳 + 机器 ID + 序列号 | 64 bit | 时间有序 | 分布式系统 |
雪花算法结构(64 bit):
符号位(1) | 时间戳(41) | 机器ID(10) | 序列号(12)
- 时间戳:毫秒级,从自定义 epoch 开始,可用约 69 年
- 机器 ID:最多 1024 个节点
- 序列号:同一毫秒内最多 4096 个 ID
import time
import threading
class Snowflake:
def __init__(self, machine_id: int, epoch: int = 1700000000000):
assert 0 <= machine_id < 1024
self.machine_id = machine_id
self.epoch = epoch
self._sequence = 0
self._last_ts = -1
self._lock = threading.Lock()
def generate(self) -> int:
with self._lock:
ts = int(time.time() * 1000) - self.epoch
if ts == self._last_ts:
self._sequence = (self._sequence + 1) & 0xFFF
if self._sequence == 0:
while ts <= self._last_ts:
ts = int(time.time() * 1000) - self.epoch
else:
self._sequence = 0
self._last_ts = ts
return (ts << 22) | (self.machine_id << 12) | self._sequence
CAP 定理
定义:分布式系统不能同时满足以下三个特性:
| 特性 | 说明 |
|---|---|
| C - 一致性(Consistency) | 所有节点在同一时刻读到的数据一致 |
| A - 可用性(Availability) | 每个请求都能收到(非错误的)响应 |
| P - 分区容错性(Partition tolerance) | 网络分区时系统仍能运行 |
在分布式环境中 P 是必须保证的,所以实际是 CP vs AP 的选择。
| 系统 | 类型 | 说明 |
|---|---|---|
| MySQL(主从复制) | CP | 主从同步期间可能不一致 |
| Redis Cluster | AP | 网络分区时优先可用性 |
| ZooKeeper | CP | 优先一致性,选主期间不可用 |
| Kafka | AP | 分区容错,最终一致 |
| Elasticsearch | AP | 优先可用,最终一致 |
| Etcd | CP | 强一致性,用于配置中心 |
BASE 理论(对 ACID 的弱化):
- BA - 基本可用(Basically Available):允许损失部分可用性
- S - 软状态(Soft State):允许中间状态存在
- E - 最终一致性(Eventually Consistent):数据最终会达到一致
消息队列使用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 异步解耦 | 生产者不需要等待消费者处理完成 | 注册后发送欢迎邮件 |
| 流量削峰 | 将瞬时高并发写入队列,消费者匀速消费 | 秒杀下单 |
| 事件驱动 | 一个事件触发多个下游处理 | 订单支付成功→库存扣减+通知+积分 |
| 日志收集 | 高吞吐写入,异步消费处理 | 访问日志→Elasticsearch |
参考:RabbitMQ完全指南、Kafka完全指南、Celery完全指南
最佳实践
系统设计面试框架(RESHADED)
| 步骤 | 内容 | 时间 |
|---|---|---|
| Requirements | 澄清功能需求和非功能需求(QPS、延迟、规模) | 5 分钟 |
| Estimation | 容量估算:QPS、存储、带宽 | 5 分钟 |
| Storage | 数据模型设计、数据库选型(关系型 vs NoSQL) | 5 分钟 |
| High-level design | 画出核心模块:客户端→LB→应用层→存储层 | 10 分钟 |
| APIs | 设计关键 API 接口,明确请求/响应格式 | 5 分钟 |
| Detailed design | 深入一个模块:缓存策略、分库、异步队列 | 15 分钟 |
| Edge cases | 故障场景、数据一致性、安全 | 5 分钟 |
| Deep dive | 回答面试官追问 | - |
容量估算速算表
| 数量级 | 含义 | 示例 |
|---|---|---|
| 1 QPS | 低流量 | 内部工具 |
| 1,000 QPS | 中等 | 小型 Web 应用 |
| 10,000 QPS | 高流量 | 大型互联网应用 |
| 100,000 QPS | 极高流量 | Twitter、微博量级 |
常用换算:
- 100 万 DAU,每用户每天 10 次请求 → 约 12 QPS(100万×10/86400)
- 1 GB/day 数据写入 → 约 12 KB/s
- MySQL 单表上限:500 万行以上考虑分表;读 QPS > 5,000 考虑读写分离
缓存设计决策树
需要缓存吗?
├── 数据读多写少 → 是,考虑缓存
│ ├── 数据允许短暂不一致 → Cache-Aside + TTL
│ ├── 数据必须强一致 → Write-Through
│ └── 写性能是瓶颈 → Write-Behind(接受丢失风险)
└── 数据写多读少 → 通常不缓存,考虑异步写入队列
数据库选型原则
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 强一致事务(订单、账户) | MySQL / PostgreSQL | ACID 保证 |
| 大规模写入、时序数据 | ClickHouse / InfluxDB | 列式存储,压缩率高 |
| 缓存、会话、排行榜 | Redis | 内存级速度,丰富数据结构 |
| 文档型(商品、内容) | MongoDB | 灵活 Schema,嵌套文档 |
| 全文检索、聚合分析 | Elasticsearch | 倒排索引,分布式查询 |
| 图关系(社交、权限) | Neo4j | 原生图遍历 |
常见陷阱
陷阱:缓存与数据库数据不一致
现象: 用户更新了数据,缓存命中后返回旧数据,直到缓存过期才恢复正常。
原因: 更新数据库后忘记同步删除/更新缓存,或更新缓存失败(网络抖动)导致脏数据残留。
解决: 使用 Cache-Aside(旁路缓存)模式:写操作先更新数据库,再删除缓存(而不是更新缓存);读操作 miss 时从数据库加载并写入缓存。删除缓存比更新缓存更安全,因为下次读时才重建,避免并发写的竞态。
陷阱:没有设置超时导致线程/协程堆积
现象: 下游服务变慢后,上游服务接口响应时间线性增加,最终所有线程耗尽,服务不可用。
原因: 对数据库、Redis、HTTP 等外部调用未设置超时,一旦下游慢,调用方线程长时间阻塞,新请求无线程可用。
解决: 所有外部调用必须配置超时(HTTP: timeout=3s,数据库: connect_timeout + read_timeout,Redis: socket_timeout);配合熔断器(Circuit Breaker)在错误率超阈值时快速失败,防止级联故障。
陷阱:分布式锁过期时任务未完成
现象: 任务持有 Redis 分布式锁,但任务执行时间超过锁的 TTL,锁被自动释放,其他节点拿到锁并开始执行,产生重复操作。
原因: TTL 设置过短,或任务执行时间不可预估(GC 暂停、慢查询等)导致持锁时间超出预期。
解决: 用看门狗(watchdog)机制:持有锁的进程周期性续期;或在任务逻辑中做幂等保证(即使重复执行也不影响最终结果),锁只是降低重复的概率而非完全依赖。
参见
- Redis完全指南 — 缓存实现、分布式锁、排行榜
- MySQL基础完全指南 — 索引优化、分库分表
- Kafka完全指南 — 消息队列、流量削峰
- RabbitMQ完全指南 — 任务队列、事件驱动
- Prometheus与Grafana监控 — 系统性能监控、SLA 告警