跳转至

⚙️ 核心组件详解

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

系统设计核心组件关系图

学习时间:6小时 | 难度:⭐⭐⭐ 中等 | 前置知识:01-系统设计方法论


🎯 本章目标

  • 深入理解负载均衡的原理与算法选型
  • 掌握缓存策略与淘汰算法
  • 理解CDN的工作原理
  • 对比主流消息队列的特性与适用场景
  • 建立数据库选型的决策框架
  • 掌握分布式ID生成方案
  • 实现常见限流算法

📋 目录


1. 负载均衡

1.1 概述

负载均衡(Load Balancer)将流量分发到多个后端服务器,实现: - 高可用:一台服务器挂了,流量自动转移到其他服务器 - 水平扩展:通过增加服务器提升整体处理能力 - 故障隔离:自动将故障服务器从池中摘除

Text Only
                    ┌──────────────┐
                    │   Client     │
                    └──────┬───────┘
                    ┌──────▼───────┐
                    │  Load        │
                    │  Balancer    │
                    └──┬───┬───┬──┘
                       │   │   │
              ┌────────┘   │   └────────┐
              │            │            │
        ┌─────▼──┐   ┌────▼───┐  ┌─────▼──┐
        │Server 1│   │Server 2│  │Server 3│
        └────────┘   └────────┘  └────────┘

1.2 L4 vs L7 负载均衡

特性 L4(传输层) L7(应用层)
工作层 TCP/UDP HTTP/HTTPS
依据 IP、端口 URL、Header、Cookie
性能 高(不解析内容) 较低(需解析HTTP)
灵活性 高(可按路径/域名分发)
SSL终止 不支持 支持
会话保持 基于IP 基于Cookie
典型产品 LVS、F5、AWS NLB Nginx、HAProxy、AWS ALB
适用场景 高吞吐、简单路由 Web应用、微服务
Text Only
L4负载均衡(基于IP和端口):
Client → [LB看到: srcIP:port → dstIP:port] → Server
        只关注传输层信息,不解析应用层内容

L7负载均衡(基于应用层内容):
Client → [LB看到: GET /api/users HTTP/1.1
                   Host: example.com
                   Cookie: session=abc]
        可以根据URL路径、域名、Header来路由

例如:
  /api/*    → API Server集群
  /static/* → 静态资源服务器
  /ws/*     → WebSocket服务器

1.3 负载均衡算法

1.3.1 轮询(Round Robin)

Text Only
请求序列:
  请求1 → Server A
  请求2 → Server B
  请求3 → Server C
  请求4 → Server A  ← 循环
  请求5 → Server B
  ...

优势:实现简单,绝对公平 劣势:不考虑服务器性能差异和当前负载 适用:服务器配置相同、请求处理时间均匀

Python
class RoundRobinBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.index = 0

    def get_server(self):
        server = self.servers[self.index]
        self.index = (self.index + 1) % len(self.servers)
        return server

1.3.2 加权轮询(Weighted Round Robin)

Text Only
权重配置:
  Server A: weight=5(高性能)
  Server B: weight=3(中性能)
  Server C: weight=2(低性能)

请求分配比例:A:B:C = 5:3:2
  10个请求中:A处理5个,B处理3个,C处理2个

优势:考虑了服务器性能差异 劣势:权重需要手动配置,不能动态调整 适用:服务器配置不同的集群

Python
class WeightedRoundRobinBalancer:
    def __init__(self, servers_with_weights):
        """servers_with_weights: [('A', 5), ('B', 3), ('C', 2)]"""
        self.servers = servers_with_weights
        self.current_weights = [0] * len(servers_with_weights)
        self.total_weight = sum(w for _, w in servers_with_weights)

    def get_server(self):
        # Nginx smooth weighted round robin
        max_weight = -1
        max_index = 0
        for i, (server, weight) in enumerate(self.servers):  # enumerate同时获取索引和值
            self.current_weights[i] += weight
            if self.current_weights[i] > max_weight:
                max_weight = self.current_weights[i]
                max_index = i

        self.current_weights[max_index] -= self.total_weight
        return self.servers[max_index][0]

1.3.3 最小连接数(Least Connections)

Text Only
当前连接数:
  Server A: 10个活跃连接
  Server B: 3个活跃连接
  Server C: 7个活跃连接

新请求 → Server B(连接数最少)

优势:动态感知服务器负载 劣势:需要维护连接计数,慢启动问题 适用:请求处理时间差异大的场景(如视频转码)

Python
import heapq
from collections import defaultdict  # defaultdict带默认值的字典,避免KeyError

class LeastConnectionsBalancer:
    def __init__(self, servers):
        self.connections = {s: 0 for s in servers}

    def get_server(self):
        server = min(self.connections, key=self.connections.get)
        self.connections[server] += 1
        return server

    def release_server(self, server):
        self.connections[server] -= 1

1.3.4 一致性哈希(Consistent Hashing)

一致性哈希是分布式系统中最重要的算法之一,用于将请求/数据映射到特定服务器。

Text Only
哈希环(0 ~ 2^32-1):

              0
            ╱   ╲
          ╱       ╲
        S1          ╲
       ╱              S2
      │                │
      │    Hash Ring    │
      │                │
       ╲              ╱
        S3          ╱
          ╲       ╱
            ╲   ╱
              S4(虚拟)

Key的映射过程:
  1. hash(key) → 得到哈希值
  2. 在环上顺时针找到最近的Server
  3. key1 → S1, key2 → S2, key3 → S3

解决的问题:普通哈希(hash % N)在增删节点时需要大量数据迁移

Text Only
普通哈希:hash(key) % 3
  节点从3个变成4个,几乎所有key都需要重新映射!

一致性哈希:
  增加一个节点,只影响它和它后一个节点之间的数据
  约 1/N 的数据需要迁移

虚拟节点:解决数据倾斜问题

Python
import hashlib
import bisect

class ConsistentHash:
    def __init__(self, nodes=None, replicas=150):
        """replicas: 每个真实节点的虚拟节点数"""
        self.replicas = replicas
        self.ring = {}       # hash_value -> node
        self.sorted_keys = []  # 排序的hash值列表

        if nodes:
            for node in nodes:
                self.add_node(node)

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node):
        for i in range(self.replicas):
            virtual_key = f"{node}:v{i}"
            hash_val = self._hash(virtual_key)
            self.ring[hash_val] = node
            bisect.insort(self.sorted_keys, hash_val)

    def remove_node(self, node):
        for i in range(self.replicas):
            virtual_key = f"{node}:v{i}"
            hash_val = self._hash(virtual_key)
            del self.ring[hash_val]
            self.sorted_keys.remove(hash_val)

    def get_node(self, key):
        if not self.ring:
            return None
        hash_val = self._hash(key)
        idx = bisect.bisect_right(self.sorted_keys, hash_val)
        if idx == len(self.sorted_keys):
            idx = 0
        return self.ring[self.sorted_keys[idx]]

1.3.5 算法对比总结

算法 复杂度 状态 公平性 适用场景
轮询 O(1) 无状态 绝对公平 服务器配置相同
加权轮询 O(N) 无状态 按权重公平 配置不同
最小连接 O(N) 有状态 动态公平 长连接/处理时间不均
一致性哈希 O(logN) 有状态 基于哈希 有状态服务/缓存
IP哈希 O(1) 无状态 基于IP 会话保持
随机 O(1) 无状态 概率公平 简单场景

2. 缓存

2.1 缓存的价值

Text Only
无缓存:                          有缓存:
Client → Server → Database        Client → Server → Cache(命中) → 返回
                                                   → Cache(未命中) → DB → 存入Cache
延迟:~100ms                       延迟:~1ms(命中) / ~100ms(未命中)

缓存的核心价值:
  - 响应时间从 100ms → 1ms
  - 数据库压力降低 80-95%
  - 整体吞吐量大幅提升

2.2 缓存策略

Cache Aside(旁路缓存)— 最常用

Text Only
读流程:                           写流程:
│                                 │
├── 1. 查缓存                     ├── 1. 更新数据库
│   ├── 命中 → 返回               ├── 2. 删除缓存
│   └── 未命中                    └── 完成
│       ├── 2. 查数据库
│       ├── 3. 写入缓存
│       └── 4. 返回

优势:实现简单,适合读多写少 劣势:首次访问一定未命中;短暂不一致窗口 适用:大部分Web应用场景

Python
class CacheAsideService:
    def __init__(self, cache, db):
        self.cache = cache
        self.db = db

    def get(self, key):
        # 1. 查缓存
        value = self.cache.get(key)
        if value is not None:
            return value  # 命中

        # 2. 未命中,查数据库
        value = self.db.get(key)
        if value is not None:
            # 3. 写入缓存
            self.cache.set(key, value, ttl=300)
        return value

    def update(self, key, value):
        # 1. 更新数据库
        self.db.update(key, value)
        # 2. 删除缓存(而不是更新缓存!)
        self.cache.delete(key)

💡 为什么删除缓存而不是更新缓存? 因为并发更新时,可能出现缓存和数据库不一致。删除缓存更安全。

Write Through(写穿透缓存)

Text Only
读流程:                           写流程:
│                                 │
├── 1. 查缓存                     ├── 1. 写缓存
│   ├── 命中 → 返回               └── 2. 缓存同步写数据库
│   └── 未命中                         (作为整体操作)
│       ├── 2. 缓存从DB加载
│       └── 3. 返回

优势:数据一致性强 劣势:写延迟高(同步写DB),不常用数据也在缓存中 适用:对一致性要求高的场景

Write Behind(写回缓存)

Text Only
读流程:                           写流程:
│                                 │
├── 1. 查缓存                     ├── 1. 只写缓存(快速返回)
│   ├── 命中 → 返回               └── 2. 异步批量写入数据库
│   └── 未命中
│       ├── 2. 缓存从DB加载
│       └── 3. 返回

优势:写性能极高 劣势:可能丢数据(缓存挂了还没来得及写DB) 适用:写密集型场景(如日志、计数器)

Read Through(读穿透缓存)

Text Only
读流程:
├── 1. 查缓存
│   ├── 命中 → 返回
│   └── 未命中
│       ├── 2. 缓存自动从DB加载
│       └── 3. 返回
│  与Cache Aside的区别:
│  Cache Aside: 应用程序负责加载
│  Read Through: 缓存组件负责加载

策略对比总结

策略 读性能 写性能 一致性 复杂度 适用场景
Cache Aside 最终一致 通用场景
Write Through 强一致 一致性优先
Write Behind 极高 写密集型
Read Through 最终一致 读密集型

2.3 缓存淘汰策略

当缓存空间满时,需要淘汰旧数据:

策略 全称 原理 优势 劣势
LRU Least Recently Used 淘汰最近最少使用的 实现简单,效果好 偶尔访问会污染
LFU Least Frequently Used 淘汰使用频率最低的 适合有热点的场景 新数据难以被保留
TTL Time To Live 按过期时间淘汰 简单高效 可能淘汰热数据
FIFO First In First Out 淘汰最先进入的 最简单 不考虑使用频率
Random 随机淘汰 随机选择淘汰 无额外开销 可能淘汰热数据

LRU实现(HashMap + 双向链表):

Python
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache:
            return None
        self.cache.move_to_end(key)  # 移到末尾(最近使用)
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # 移除最久未使用
Java
// Java实现
public class LRUCache<K, V> extends LinkedHashMap<K, V> {  // extends继承;implements实现接口
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // true = 按访问顺序
        this.capacity = capacity;
    }

    @Override  // @Override重写父类方法
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

2.4 缓存常见问题

问题 描述 解决方案
缓存穿透 查询不存在的数据,每次都打到DB 布隆过滤器 / 缓存空值
缓存击穿 热点key过期,大量请求打到DB 互斥锁 / 热点key不过期
缓存雪崩 大量key同时过期,DB瞬间压力暴增 过期时间加随机值 / 多级缓存
缓存不一致 缓存和DB数据不一致 延迟双删 / 订阅binlog
Text Only
缓存穿透:                      解决1:布隆过滤器
请求 → 缓存(miss) → DB(miss)    请求 → 布隆过滤器 → 不存在 → 直接返回
请求 → 缓存(miss) → DB(miss)                     → 可能存在 → 查缓存/DB
(恶意攻击或爬虫)

                                解决2:缓存空值
                                请求 → 缓存(miss) → DB(miss) → 缓存空值(TTL较短)
                                再次请求 → 缓存(hit, 空值) → 返回空

3. CDN

3.1 CDN工作原理

CDN(Content Delivery Network)将内容缓存到离用户最近的边缘节点。

Text Only
无CDN:                          有CDN:
用户(北京)                       用户(北京)
  │                               │
  │ 200ms                         │ 5ms
  │                               ▼
  │                         ┌────────────┐
  │                         │  CDN边缘节点 │ ← 北京节点
  │                         │  (缓存命中)  │
  │                         └────────────┘
  ▼                               │ (缓存未命中时)
┌────────────┐                    │ 100ms
│  源站服务器  │ ← 广州              ▼
└────────────┘              ┌────────────┐
                            │  源站服务器  │ ← 广州
                            └────────────┘

3.2 CDN的类型

类型 缓存内容 示例
Pull CDN CDN自动从源站拉取 普通网站图片
Push CDN 主动推送到CDN 大型文件、视频

3.3 CDN适用场景

适合CDN 不适合CDN
静态资源(图片/JS/CSS) 动态API响应
视频流 个性化内容
大文件下载 实时数据
高流量网站 低流量内部系统

3.4 CDN关键配置

配置 说明 建议值
TTL 缓存过期时间 静态资源: 1天~1年
Cache Key 缓存键 URL + 必要参数
回源策略 缓存未命中时的源站请求 负载均衡到多个源站
HTTPS 是否支持HTTPS 必须支持
压缩 Gzip/Brotli 开启

4. 消息队列

4.1 为什么需要消息队列

Text Only
同步调用(紧耦合):                异步调用(松耦合):
订单服务 → 库存服务               订单服务 → MQ → 库存服务
         → 积分服务                          → 积分服务
         → 通知服务                          → 通知服务

问题:                             优势:
- 任一下游挂了整个失败             - 下游挂了不影响上游
- 响应时间是所有下游之和           - 上游快速返回
- 增加新下游需要改上游代码         - 新增消费者无需改上游

消息队列三大价值

价值 说明 示例
解耦 上下游互不依赖 订单→库存→积分
异步 快速返回,后台处理 上传视频→异步转码
削峰 缓冲突发流量 秒杀请求排队处理

4.2 Kafka vs RabbitMQ vs RocketMQ

特性 Kafka RabbitMQ RocketMQ
定位 分布式流处理平台 企业级消息代理 金融级消息中间件
开发语言 Scala/Java Erlang Java
吞吐量 百万级/s 万级/s 十万级/s
延迟 ms级 μs级 ms级
消息模型 Pull(拉模式) Push/Pull Pull
消息保留 持久化到磁盘,按时间保留 消费后删除 持久化
顺序性 Partition内有序 不保证 Queue内有序
事务消息 0.11+支持 支持 原生支持
延迟消息 需自己实现 插件支持 原生18级
消息回溯 支持(Offset指定) 不支持 支持
社区 Apache基金会 Pivotal/VMware Apache基金会(阿里)
运维复杂度 高(ZK/KRaft)

选型建议

Text Only
选择Kafka:
  ✅ 大数据场景(日志收集、数据管道)
  ✅ 高吞吐量需求(100K+ msg/s)
  ✅ 需要消息回溯和流处理
  ✅ 事件溯源架构

选择RabbitMQ:
  ✅ 企业级应用集成
  ✅ 复杂路由需求(Exchange模式)
  ✅ 对延迟敏感(μs级)
  ✅ 团队熟悉Erlang生态

选择RocketMQ:
  ✅ 国内电商/金融场景
  ✅ 需要事务消息
  ✅ 需要延迟消息
  ✅ 阿里云生态

4.3 消息队列核心概念

Text Only
Producer(生产者)
    │ 发送消息
┌────────────────────────────┐
│        Broker(消息代理)      │
│  ┌──────┐ ┌──────┐ ┌──────┐│
│  │Topic1│ │Topic2│ │Topic3││
│  │      │ │      │ │      ││
│  │ P0   │ │ P0   │ │ P0   ││ ← Partition(分区)
│  │ P1   │ │ P1   │ │ P1   ││
│  │ P2   │ │      │ │      ││
│  └──────┘ └──────┘ └──────┘│
└────────────────────────────┘
    │ 消费消息
Consumer Group(消费者组)
  Consumer 1  Consumer 2  Consumer 3

4.4 消息可靠性保证

保证级别 说明 代价
At Most Once 最多一次,可能丢失 最高性能
At Least Once 至少一次,可能重复 中等性能
Exactly Once 精确一次,不丢不重 最低性能

5. 数据库选型

5.1 数据库类型全景

Text Only
数据库选型决策树:

                    数据有明确的Schema吗?
                   ╱                     ╲
                 是                        否
                ╱                           ╲
          需要复杂查询/事务?            数据是什么形态?
          ╱              ╲             ╱    │     ╲
        是                否         KV   文档   图关系
        │                 │          │     │      │
   ┌────▼────┐      ┌────▼────┐  ┌──▼──┐ ┌▼───┐ ┌▼──────┐
   │关系型数据库│      │列族数据库│  │Redis│ │Mongo││Neo4j │
   │ MySQL   │      │HBase   │  │     │ │DB  │ │      │
   │ PgSQL   │      │Cassandra│  └─────┘ └────┘ └──────┘
   └─────────┘      └─────────┘

5.2 四类数据库对比

类型 代表 数据模型 优势 劣势 适用场景
关系型 MySQL, PostgreSQL 表(行+列) ACID事务、SQL灵活 扩展性差、Schema固定 事务型业务
文档型 MongoDB, CouchDB JSON文档 Schema灵活、开发快 不适合复杂Join 内容管理、配置
列族型 HBase, Cassandra 列族 高写入、大数据量 查询模式受限 时序数据、日志
图数据库 Neo4j, JanusGraph 节点+边 关系查询极快 通用查询慢 社交网络、推荐

5.3 关系型数据库选型

特性 MySQL PostgreSQL
并发性能 高(简单查询) 高(复杂查询)
JSON支持 5.7+ 基本支持 原生优秀支持
全文搜索 支持 原生强大
地理信息 基本 PostGIS强大
复制 主从 主从+逻辑复制
生态 国内最广泛 全球增长最快
适用 互联网业务 复杂数据分析

5.4 NoSQL选型指南

Text Only
高性能缓存       → Redis (KV, 内存)
              → Memcached (纯缓存)

灵活Schema文档   → MongoDB (通用文档)
              → CouchDB (离线优先)

大规模时序/日志  → Cassandra (AP, 高写入)
              → HBase (CP, Hadoop生态)
              → InfluxDB (时序专用)

社交图谱关系    → Neo4j (原生图)
              → JanusGraph (分布式图)

全文搜索       → Elasticsearch (搜索+分析)
              → Solr (传统搜索)

6. 分布式ID

6.1 为什么需要分布式ID

分库分表后,数据库自增ID不再全局唯一,需要全局唯一的ID生成方案。

分布式ID的要求: - 全局唯一 - 趋势递增(有利于B+树索引) - 高可用 - 高性能

6.2 方案对比

方案 唯一性 有序性 可用性 性能 长度 适用场景
UUID ❌ 无序 ✅ 本地生成 128bit(长) 对顺序无要求
数据库自增 ❌ 单点 64bit 小规模
Snowflake ✅ 趋势递增 ✅ 本地生成 64bit 通用方案
Leaf(美团) 64bit 大厂方案

6.3 Snowflake算法详解

Text Only
Snowflake ID结构(64bit):

 0 | 00000000 00000000 00000000 00000000 00000000 0 | 00000 | 00000 | 000000000000
 │  │                                              │  │       │       │
 │  │                                              │  │       │       └── 序列号(12bit)
 │  │                                              │  │       │           每ms最多4096个
 │  │                                              │  │       │
 │  │                                              │  │       └── 机器ID(5bit)
 │  │                                              │  │           最多32台
 │  │                                              │  │
 │  │                                              │  └── 数据中心ID(5bit)
 │  │                                              │       最多32个
 │  │                                              │
 │  └── 时间戳(41bit)                               │
 │      从起始时间(epoch)开始的毫秒数                   │
 │      可用 2^41 ms ≈ 69年                         │
 │                                                  │
 └── 符号位(1bit),固定为0
Python
import time
import threading

class SnowflakeGenerator:
    # 起始时间戳 (2020-01-01 00:00:00 UTC)
    EPOCH = 1577836800000

    # 各部分位数
    WORKER_ID_BITS = 5
    DATACENTER_ID_BITS = 5
    SEQUENCE_BITS = 12

    # 最大值
    MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1      # 31
    MAX_DATACENTER_ID = (1 << DATACENTER_ID_BITS) - 1  # 31
    MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1          # 4095

    # 位移量
    WORKER_ID_SHIFT = SEQUENCE_BITS                  # 12
    DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS  # 17
    TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS  # 22

    def __init__(self, datacenter_id, worker_id):
        if datacenter_id > self.MAX_DATACENTER_ID or datacenter_id < 0:
            raise ValueError(f"Datacenter ID must be between 0 and {self.MAX_DATACENTER_ID}")
        if worker_id > self.MAX_WORKER_ID or worker_id < 0:
            raise ValueError(f"Worker ID must be between 0 and {self.MAX_WORKER_ID}")

        self.datacenter_id = datacenter_id
        self.worker_id = worker_id
        self.sequence = 0
        self.last_timestamp = -1
        self.lock = threading.Lock()

    def _current_millis(self):
        return int(time.time() * 1000)

    def next_id(self):
        with self.lock:
            timestamp = self._current_millis()

            if timestamp == self.last_timestamp:
                self.sequence = (self.sequence + 1) & self.MAX_SEQUENCE
                if self.sequence == 0:
                    # 序列号用完,等待下一毫秒
                    while timestamp <= self.last_timestamp:
                        timestamp = self._current_millis()
            else:
                self.sequence = 0

            if timestamp < self.last_timestamp:
                raise Exception("Clock moved backwards!")

            self.last_timestamp = timestamp

            return (
                ((timestamp - self.EPOCH) << self.TIMESTAMP_SHIFT) |
                (self.datacenter_id << self.DATACENTER_ID_SHIFT) |
                (self.worker_id << self.WORKER_ID_SHIFT) |
                self.sequence
            )

# 使用示例
generator = SnowflakeGenerator(datacenter_id=1, worker_id=1)
id1 = generator.next_id()
id2 = generator.next_id()

7. 限流算法

7.1 为什么需要限流

Text Only
正常流量:                突发流量/攻击:
  ||||                   |||||||||||||||||||||
  ↓↓↓↓                  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
┌──────┐               ┌──────┐
│Server│ ← OK          │Server│ ← 崩溃!
└──────┘               └──────┘

加上限流器:
|||||||||||||||||||||
  ┌──────────┐
  │ Rate     │ → 超出限制的请求被拒绝(429)
  │ Limiter  │
  └────┬─────┘
       ↓ (只放行合理量的请求)
  ||||
  ↓↓↓↓
┌──────┐
│Server│ ← 安全
└──────┘

7.2 令牌桶算法(Token Bucket)

Text Only
原理:
  - 以固定速率往桶里放令牌
  - 请求来时取一个令牌
  - 桶满了令牌就丢弃
  - 没有令牌的请求被拒绝

  ┌─────────────────┐
  │   Token Bucket   │
  │  ○ ○ ○ ○ ○      │  ← 最多capacity个令牌
  │  ○ ○ ○ ○        │
  │                  │
  └────────┬─────────┘
           │ ↑
    取令牌  │ │ 定期放令牌
           ↓ │
        ┌────────┐
        │ Request│
        └────────┘

特点:
  - 允许突发流量(桶里有存量令牌)
  - 稳定的平均速率
Python
import time
import threading

class TokenBucket:
    def __init__(self, rate, capacity):
        """
        rate: 每秒生成令牌速率
        capacity: 桶的最大容量
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity  # 初始满桶
        self.last_time = time.time()
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            now = time.time()
            # 补充令牌
            elapsed = now - self.last_time
            self.tokens = min(
                self.capacity,
                self.tokens + elapsed * self.rate
            )
            self.last_time = now

            # 尝试消耗一个令牌
            if self.tokens >= 1:
                self.tokens -= 1
                return True
            return False

# 使用:每秒10个请求,允许突发20个
limiter = TokenBucket(rate=10, capacity=20)

# 模拟请求
for i in range(25):
    if limiter.allow_request():
        print(f"Request {i}: Allowed")
    else:
        print(f"Request {i}: Rejected")
Go
// Go实现
package ratelimit

import (
    "sync"
    "time"
)

type TokenBucket struct {
    rate       float64
    capacity   float64
    tokens     float64
    lastTime   time.Time
    mu         sync.Mutex
}

func NewTokenBucket(rate, capacity float64) *TokenBucket {
    return &TokenBucket{
        rate:     rate,
        capacity: capacity,
        tokens:   capacity,
        lastTime: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()  // defer延迟执行,函数返回前调用

    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens += elapsed * tb.rate
    if tb.tokens > tb.capacity {
        tb.tokens = tb.capacity
    }
    tb.lastTime = now

    if tb.tokens >= 1 {
        tb.tokens -= 1
        return true
    }
    return false
}

7.3 漏桶算法(Leaky Bucket)

Text Only
原理:
  - 请求进入桶中排队
  - 以固定速率处理请求
  - 桶满了新请求被丢弃

       请求进入
  ┌─────────────┐
  │ ○ ○ ○ ○     │  ← 队列(有最大容量)
  │ ○ ○ ○       │
  │ ○ ○         │
  └──────┬──────┘
      ╔══╧══╗
      ║ 漏口 ║  ← 固定速率流出
      ╚══╤══╝
    ○   ○   ○   ← 匀速处理

特点:
  - 输出速率恒定(匀速)
  - 不允许突发流量
  - 适合对处理速率有严格要求的场景
Python
import time
import threading
from collections import deque

class LeakyBucket:
    def __init__(self, rate, capacity):
        """
        rate: 每秒处理请求数(漏出速率)
        capacity: 桶的最大容量
        """
        self.rate = rate
        self.capacity = capacity
        self.water = 0  # 当前水量(排队请求数)
        self.last_time = time.time()
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            now = time.time()
            elapsed = now - self.last_time

            # 漏掉一些水
            self.water = max(0, self.water - elapsed * self.rate)
            self.last_time = now

            # 尝试加水
            if self.water < self.capacity:
                self.water += 1
                return True
            return False

7.4 滑动窗口算法(Sliding Window)

Text Only
固定窗口的问题:
  窗口1: [00:00 ~ 01:00]  99个请求
  窗口2: [01:00 ~ 02:00]  99个请求
  限制: 100个/分钟

  但是在 [00:30 ~ 01:30] 这个时间段内有 198个请求!
  超出了限制!

滑动窗口解决方案:
  ┌────────────────────────────────┐
  │        滑动窗口(1分钟)         │
  │  [当前时间 - 60s, 当前时间]     │
  └────────────────────────────────┘

  每次请求来时,只计算最近60秒内的请求数
Python
import time
import threading  # 线程池/多线程:并发执行任务
from collections import deque

class SlidingWindowLog:
    """滑动窗口日志算法 - 精确但内存开销大"""
    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = deque()  # 存储每个请求的时间戳
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            now = time.time()
            # 清除窗口外的请求
            while self.requests and self.requests[0] < now - self.window_seconds:
                self.requests.popleft()

            if len(self.requests) < self.max_requests:
                self.requests.append(now)
                return True
            return False

class SlidingWindowCounter:
    """滑动窗口计数器 - 近似但内存省"""
    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.current_count = 0
        self.previous_count = 0
        self.current_start = time.time()
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            now = time.time()
            elapsed = now - self.current_start

            if elapsed >= self.window_seconds:
                self.previous_count = self.current_count
                self.current_count = 0
                self.current_start = now
                elapsed = 0

            # 加权计算
            weight = 1 - (elapsed / self.window_seconds)
            estimated_count = self.previous_count * weight + self.current_count

            if estimated_count < self.max_requests:
                self.current_count += 1
                return True
            return False

7.5 限流算法对比

算法 突发流量 速率平滑 内存 精确度 适用场景
令牌桶 ✅允许 O(1) API限流(最常用)
漏桶 ❌不允许 ✅匀速 O(1) 请求处理平滑化
固定窗口 有边界问题 O(1) 简单统计
滑动窗口日志 无边界问题 O(N) 最高 精确限流
滑动窗口计数 无边界问题 O(1) 较高 通用限流

8. 练习与延伸阅读

8.1 练习题

  1. 负载均衡:实现一个支持权重动态调整的负载均衡器
  2. 缓存:设计一个支持TTL的LRU缓存(同时支持按时间和按容量淘汰)
  3. 消息队列选型:为以下场景选择合适的消息队列并说明理由:
  4. 用户行为日志收集系统
  5. 电商订单处理系统
  6. 即时通讯消息分发系统
  7. 分布式ID:分析Snowflake算法的时钟回拨问题,设计解决方案
  8. 限流:实现一个分布式限流器(基于Redis)

8.2 延伸阅读

  • 《DDIA》第5章 — 复制
  • 《DDIA》第6章 — 分区
  • Nginx负载均衡官方文档
  • Redis官方文档 — Caching patterns
  • Kafka设计文档
  • Google Guava RateLimiter源码分析

📝 本章小结

组件 核心要点 面试常见问题
负载均衡 L4/L7选型、算法选择 一致性哈希原理
缓存 Cache Aside最常用、LRU实现 缓存穿透/击穿/雪崩
CDN Pull/Push、适合静态资源 CDN如何更新缓存
消息队列 Kafka大吞吐、RocketMQ事务消息 消息丢失/重复如何处理
数据库 按数据特征选型 关系型vs文档型场景
分布式ID Snowflake最通用 时钟回拨问题
限流 令牌桶最常用 手写限流算法

上一章:系统设计方法论 | 下一章:数据存储设计