系统设计与大规模架构学习笔记

从分布式理论到经典系统设计题,系统理解大规模互联网架构设计。


1 · 分布式基础理论

1.1 CAP 定理

CAP 定理指出,分布式系统最多同时满足以下三项中的两项:

  • C(Consistency)一致性:所有节点同一时刻看到相同的数据
  • A(Availability)可用性:每个请求都能收到非错误响应
  • P(Partition Tolerance)分区容错性:网络分区时系统仍能继续运行

为什么 P 必须满足?

因为网络分区在分布式系统中不可避免(机器宕机、网线断开、机房故障),所以分布式系统实际上是在 C 和 A 之间做选择。

CP 系统:

  • 优先保证一致性
  • 网络分区时宁可拒绝服务
  • 代表:ZooKeeper、etcd、HBase

AP 系统:

  • 优先保证可用性
  • 网络分区时允许暂时不一致
  • 代表:Eureka、Cassandra、DynamoDB

图 1 \xb7 CAP 定理

1.2 BASE 理论

BASE 是 CAP 中 AP 方向的工程实践:

  • Basically Available:基本可用,允许部分降级
  • Soft State:软状态,允许中间状态
  • Eventually Consistent:最终一致性,数据最终会变得一致

例如电商系统:下单后立即显示"待支付",库存稍后扣减,这就是最终一致性。

1.3 一致性模型

强一致性:

  • 写入成功后,任何后续读取都能读到最新值
  • 实现代价高

最终一致性:

  • 写入后经过一段时间,所有副本一致
  • 中间可能读到旧值

因果一致性:

  • 有因果关系的操作保持顺序
  • 无关操作可以乱序

读己之写(Read Your Writes):

  • 自己写入的数据,自己后续读取一定能读到

2 · 共识算法

2.1 Raft

Raft 是一种易于理解的共识算法,用于在分布式系统中达成一致。

三种角色:

  • Leader:处理所有客户端请求
  • Follower:被动接收 Leader 日志
  • Candidate:选举期间临时角色

Leader 选举:

  1. 每个节点随机超时(150ms ~ 300ms)
  2. 超时后成为 Candidate,给自己投票,向其他节点要票
  3. 获得多数票成为 Leader

日志复制:

  1. Leader 接收客户端请求,追加到本地日志
  2. Leader 并行发送给所有 Follower
  3. 多数 Follower 确认后提交
  4. Leader 通知 Follower 已提交

安全性:

  • 已提交的日志不会丢失
  • Leader 崩溃后,新 Leader 一定包含所有已提交日志

代表应用:etcd、Consul、TiKV。

2.2 Paxos

Paxos 是 Leslie Lamport 提出的经典共识算法。

角色:

  • Proposer:提出提案
  • Acceptor:对提案投票
  • Learner:学习已通过的值

Paxos 分为两个阶段:Prepare 和 Accept。

Multi-Paxos 优化:选出一个稳定 Leader,减少两轮通信。

2.3 Raft vs Paxos

特性 Raft Paxos
理解难度 低 高
工程实现 etcd / Consul Chubby / Spanner
强 Leader 是 可选
教学普及 广泛 偏理论

3 · 缓存策略

3.1 缓存模式

Cache-Aside(旁路缓存)

  • 应用先读缓存,未命中再读 DB,然后写入缓存
  • 最常用
def get_user(user_id):
    user = cache.get(user_id)
    if user is None:
        user = db.query(user_id)
        cache.set(user_id, user, ttl=3600)
    return user

Read-Through

  • 缓存层自动从 DB 加载
  • 应用只和缓存交互

Write-Through

  • 写入时同时更新缓存和 DB
  • 数据一致性更好,但写延迟增加

Write-Behind(Write-Back)

  • 先写缓存,异步写 DB
  • 吞吐高,但有数据丢失风险

3.2 缓存三大问题

缓存穿透

  • 查询不存在的数据,请求直接打到 DB
  • 解决:布隆过滤器、缓存空值

缓存击穿

  • 热点 key 过期,大量请求同时查 DB
  • 解决:互斥锁、热点 key 永不过期、随机过期时间

缓存雪崩

  • 大量 key 同时过期
  • 解决:过期时间加随机值、多级缓存、熔断限流

3.3 缓存淘汰策略

  • LRU(Least Recently Used):最近最少使用
  • LFU(Least Frequently Used):最不常用
  • TTL(Time To Live):按过期时间淘汰
  • Random:随机淘汰

4 · 限流算法

4.1 固定窗口计数器

把时间分成固定窗口,每个窗口计数。

窗口 1:0-1s,限制 100 请求 窗口 2:1-2s,限制 100 请求

缺点:窗口边界可能有 2 倍流量突刺。

4.2 滑动窗口

把窗口细分为多个小格子,滑动统计。

更平滑,但内存开销略大。

4.3 漏桶算法

请求像水滴一样进入桶,桶以固定速率漏水。

  • 桶满则拒绝请求
  • 强制匀速输出
  • 适合流量整形

4.4 令牌桶算法

以固定速率往桶里放令牌,请求需要消耗令牌。

  • 桶空时请求被拒绝或等待
  • 允许突发流量(桶里积累了令牌)
  • 最常用,Guava RateLimiter、Sentinel 都用它
// Guava 示例
RateLimiter limiter = RateLimiter.create(100.0); // 每秒 100 个
limiter.acquire(); // 获取令牌,阻塞等待

4.5 限流维度

  • 全局限流:保护整个系统
  • 用户级限流:每个用户 / IP 限制
  • 接口级限流:不同接口不同阈值
  • 分布式限流:用 Redis + Lua 脚本保证原子性

5 · 分库分表

5.1 为什么要分

单库单表的瓶颈:

  • 数据量过大:查询慢,索引膨胀
  • 并发写入:锁竞争
  • 单表行数超过千万级性能下降明显

5.2 分片策略

范围分片:

  • 按 ID 范围或时间范围
  • 例如 user_0 存 1-1000万,user_1 存 1000万-2000万
  • 优点:扩容简单
  • 缺点:可能热点

哈希分片:

  • shard = hash(key) % N
  • 数据均匀
  • 扩容时需要重新分配数据

一致性哈希:

  • 节点和数据都映射到哈希环上
  • 增加/删除节点只影响相邻数据
  • 减少扩容时的数据迁移量

5.3 分片键选择

分片键应该是查询最频繁的字段:

  • 用户系统:user_id
  • 订单系统:order_id 或 user_id(按用户查订单多)

要避免跨分片查询,否则需要聚合多个分片结果。

5.4 分库分表中间件

  • ShardingSphere:Java 生态,支持分片、读写分离、加解密
  • MyCAT:Proxy 层分片
  • Vitess:YouTube 开源,MySQL 集群管理

5.5 分布式 ID

分库分表后自增 ID 不唯一,需要分布式 ID:

  • UUID:简单,但无序、太长
  • Snowflake:Twitter 开源,64 位,趋势递增
  • Leaf:美团开源,号段模式 + Snowflake
  • 数据库号段:从数据库批量申请 ID 段

6 · 分布式锁

6.1 Redis 分布式锁

SET lock_key unique_value NX PX 30000
  • NX:只有 key 不存在时才设置
  • PX 30000:30 秒后自动释放,防止死锁
  • unique_value:释放时校验,防止误释放别人的锁

释放锁(Lua 脚本保证原子):

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Redisson 看门狗:

  • 锁快要过期但业务未完成时自动续期
  • 避免业务执行时间长导致锁提前释放

6.2 ZooKeeper 分布式锁

  • 创建临时顺序节点
  • 序号最小的节点获得锁
  • 其他节点 Watch 前一个节点
  • 释放锁时删除节点 / 会话断开自动删除

优点:

  • 不存在 Redis 锁过期问题
  • 公平锁

缺点:

  • 性能不如 Redis
  • 依赖 ZooKeeper 集群

6.3 etcd 分布式锁

  • 基于 Lease 租约自动释放
  • 基于 Txn 事务原子比较版本号
resp, _ := kv.Txn(ctx).
    If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)).
    Then(clientv3.OpPut("lock", "held", clientv3.WithLease(leaseID))).
    Else(clientv3.OpGet("lock")).
    Commit()

7 · 经典系统设计题

7.1 设计短链服务

核心问题:把长 URL 转成短 URL。

方案:

  1. 发号器生成唯一 ID(自增 ID / Snowflake)
  2. 将 ID 转成 Base62(a-zA-Z0-9)
  3. 存储映射关系
  4. 用户访问短链时重定向到长 URL

存储:

  • MySQL 存映射
  • Redis 缓存热点短链

短链生成方式:

  • 自增 ID 转 Base62:简单、无冲突
  • 哈希取前 8 位:可能冲突,需处理

7.2 设计消息队列

消息队列的核心功能:

  • 异步解耦
  • 削峰填谷
  • 可靠投递

存储设计:

  • 顺序写磁盘(Kafka 用日志段)
  • 零拷贝(sendfile)提高消费性能

可靠性:

  • 生产者确认
  • 消费者确认
  • 消息持久化
  • 副本机制

代表:Kafka、RocketMQ、RabbitMQ、Pulsar。

7.3 设计 Feed 流

Feed 流 = 用户关注的人发布的内容按时间排序。

推模式(写扩散):

  • 用户发内容时,推送到所有粉丝的收件箱
  • 读时直接取自己的收件箱
  • 适合粉丝少的场景

拉模式(读扩散):

  • 用户读 Feed 时,实时拉取关注人的最新内容并排序
  • 适合大 V 场景

混合模式:

  • 普通用户用推
  • 大 V 用拉

存储:Redis Sorted Set,按时间戳排序。

7.4 设计秒杀系统

秒杀特点:

  • 瞬时高并发
  • 库存少
  • 读多写少

优化策略:

  • 静态资源 CDN 化
  • 页面倒计时、验证码、排队机制削峰
  • 库存扣减前置到 Redis,避免直接打 DB
  • 异步下单(消息队列)
  • 限流、熔断、降级

数据库防超卖:

UPDATE stock SET count = count - 1 WHERE id = 1 AND count > 0;

7.5 设计分布式文件系统

参考 HDFS:

  • NameNode:管理元数据
  • DataNode:存储实际数据块
  • 文件分成 128MB 的块
  • 每个块默认 3 副本
  • 机架感知放置副本

读写流程:

  • 写:客户端联系 NameNode 获取 DataNode 列表,流水线写入多个副本
  • 读:客户端从最近的 DataNode 读取

8 · 可观测性

8.1 三支柱

支柱 说明 工具
日志(Logging) 离散事件记录 ELK、Loki
指标(Metrics) 聚合数值 Prometheus + Grafana
链路追踪(Tracing) 请求全链路 Jaeger、SkyWalking、Zipkin

8.2 监控告警

关键指标:

  • QPS(每秒请求数)
  • 错误率
  • P50 / P95 / P99 延迟
  • CPU / 内存 / 磁盘 / 网络
  • 业务指标:订单量、支付成功率

告警原则:

  • 分级:P0(立即处理)~ P3(可延后)
  • 避免告警风暴
  • 每个告警要有处理手册
  • 通知到人且不过度打扰

8.3 SLI / SLO / SLA

  • SLI(Service Level Indicator):具体指标,如可用性 99.9%
  • SLO(Service Level Objective):目标值
  • SLA(Service Level Agreement):对用户的承诺,违约可能赔偿