系统设计基础

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)机制:持有锁的进程周期性续期;或在任务逻辑中做幂等保证(即使重复执行也不影响最终结果),锁只是降低重复的概率而非完全依赖。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog

数据结构完全参考

Python list 是动态数组(Dynamic Array): 推论:需要频繁头部操作时,用 collections.deque 替代 list。 循环链表将尾节点的 next 指向头节点。常见于约瑟夫环问题。使用时注意遍历终止条件应以初始节点为准,否则会无限循环。 栈遵循 LIFO(后进先出)原则。 队列遵循 FIFO(先进先出)原则。 Python heapq 是最小堆。实现最大堆时将值取反。 单调栈维护一个单调递增或递减的栈,用于求"下一个更大/更小元素"。 单调队列用于滑动窗口最值问题,队列中元素单调递减(求最大值)或单调递增(求最小值)。

By yellowdog