> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 系统设计基础
- URL: https://blog.vercanti.com/xi-tong-she-ji-ji-chu/
- Published: 2026-08-28T14:35:46.000Z
- Updated: 2026-08-28T14:59:34.000Z
- Description: SLA 对照表： 选择建议：无状态服务（Web 层、API 层）优先水平扩展；数据库初期垂直扩展，达到瓶颈后考虑分库分表或读写分离。 缓存穿透（查询不存在的 key，每次都打到 DB）： 缓存击穿（热点 key 过期，瞬间大量请求打到 DB）： 缓存雪崩（大量 key 同时过期，或缓存服务宕机）： 令牌桶 Python 实现： Redis 实现分布式限流（滑动窗口）： URL 命名规则： Cursor 分页响应格式： 雪花算法结构（64 bit）： 定义：分布式系统不能同时满足以下三个特性： 在分布式环境中 P 是必须保证的，所以实际是 CP vs AP
- Author: yellowdog
- Tags: 系统设计

> 官方文档：<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)`）；缓存服务高可用（主从/集群）；熔断降级

```python
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 实现**：

```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 实现分布式限流**（滑动窗口）：

```lua
-- 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 分页响应格式**：

```json
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6IDEwMH0",
    "has_next": true
  }
}

```

### 幂等设计

| HTTP 方法 | 是否天然幂等          | 实现方式                         |
| ------- | --------------- | ---------------------------- |
| GET     | 是               | \-                           |
| DELETE  | 是（返回 204 或 404） | \-                           |
| PUT     | 是               | 完整替换资源                       |
| POST    | 否               | 客户端传 Idempotency-Key 头，服务端去重 |
| PATCH   | 否（视实现而定）        | 用绝对值而非相对值                    |

```python
# 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

```python
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完全指南](https://blog.vercanti.com/rabbitmq-wan-quan-zhi-nan/)、[Kafka完全指南](https://blog.vercanti.com/kafka-wan-quan-zhi-nan/)、[Celery完全指南](https://blog.vercanti.com/celery-wan-quan-zhi-nan/)

---

## 最佳实践

### 系统设计面试框架（RESHADED）

| 步骤                    | 内容                         | 时间    |
| --------------------- | -------------------------- | ----- |
| **R**equirements      | 澄清功能需求和非功能需求（QPS、延迟、规模）    | 5 分钟  |
| **E**stimation        | 容量估算：QPS、存储、带宽             | 5 分钟  |
| **S**torage           | 数据模型设计、数据库选型（关系型 vs NoSQL） | 5 分钟  |
| **H**igh-level design | 画出核心模块：客户端→LB→应用层→存储层      | 10 分钟 |
| **A**PIs              | 设计关键 API 接口，明确请求/响应格式      | 5 分钟  |
| **D**etailed design   | 深入一个模块：缓存策略、分库、异步队列        | 15 分钟 |
| **E**dge cases        | 故障场景、数据一致性、安全              | 5 分钟  |
| **D**eep 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完全指南](https://blog.vercanti.com/redis-wan-quan-zhi-nan/) — 缓存实现、分布式锁、排行榜
- [MySQL基础完全指南](https://blog.vercanti.com/mysql-ji-chu-wan-quan-zhi-nan/) — 索引优化、分库分表
- [Kafka完全指南](https://blog.vercanti.com/kafka-wan-quan-zhi-nan/) — 消息队列、流量削峰
- [RabbitMQ完全指南](https://blog.vercanti.com/rabbitmq-wan-quan-zhi-nan/) — 任务队列、事件驱动
- [Prometheus与Grafana监控](https://blog.vercanti.com/prometheus-yu-grafana-jian-kong/) — 系统性能监控、SLA 告警