跳转至

核心基础设施与分布式组件 (Core Infrastructure & Building Blocks)

在构建任何高并发、高可用的大规模系统时,架构师往往像拼积木一样组合各种基础架构组件(Building Blocks)。熟练掌握这些核心组件的底层工作原理、架构演进路径与边界权衡,是系统设计面试与工程落地的核心能力。


1. 负载均衡 (Load Balancing)

负载均衡器(Load Balancer, LB)是分布式系统对外暴露的第一道大门,负责将进入的海量网络流量平摊到多个后端服务节点,防止单机过载。

1.1 四层 (L4) vs 七层 (L7) 负载均衡

graph LR
    Client["客户端"] --> L4["L4 负载均衡器 (LVS / F5)<br>基于 IP + TCP 端口转发<br>吞吐极高, 不解析 HTTP 报文"]
    L4 --> L7["L7 负载均衡器 (Nginx / Envoy / HAProxy)<br>基于 URL 路径 / Header / Cookie 转发<br>支持 SSL 卸载, 内容路由, Gzip 压缩"]
    L7 --> S1["后端集群: 支付服务"]
    L7 --> S2["后端集群: 用户服务"]
维度 四层负载均衡 (L4) 七层负载均衡 (L7)
OSI 网络层级 传输层(TCP / UDP) 应用层(HTTP / HTTPS / gRPC)
数据包处理机制 仅查看源 IP、目标 IP 与端口号,修改 IP/MAC 后直接转发,不拆开应用层数据包。 必须与客户端建立完整 TCP 连接,解包解析 HTTP Header、URL 路径与 Cookie。
转发性能 极高(百万级 QPS),主要消耗网络带宽,几乎无 CPU 计算。 略低(万级 \(\sim\) 十万级 QPS),需要消耗 CPU 做报文解析、正则匹配与 TLS 加解密。
应用感知能力 无感知(无法根据 URL 或 Header 做动态路由)。 强感知(支持按路径路由如 /api/v1/pay、A/B 测试、灰度发布、WAF 防护)。
典型技术实现 Linux LVS (IPVS), HAProxy (TCP 模式), AWS NLB Nginx, Envoy, Traefik, HAProxy (HTTP 模式), AWS ALB

1.2 一致性哈希算法 (Consistent Hashing)

在分布式缓存(如 Redis 集群、Memcached)和分片存储中,如何将 Key 均匀路由到 \(N\) 台服务器是一个经典难题。

传统取模哈希的灾难

若采用传统哈希公式:

\[ \text{Server Index} = \text{hash}(\text{key}) \pmod N \]

当集群扩容(\(N \to N+1\))或某节点宕机(\(N \to N-1\))时,绝大多数已缓存 Key 的映射位置都会发生剧烈偏移!导致全网缓存瞬间集体失效(命中率骤降至接近 \(0\%\)),海量流量直接穿透到底层数据库,引发系统全面雪崩。

一致性哈希环 (Consistent Hash Ring)

一致性哈希将哈希空间组织成一个首尾相接的圆环(取值范围通常为 \([0, 2^{32}-1]\)):

           0 / 2^32 - 1
             Node 1 (Hash A)
         /              \
        /                \
    Key 1                 Node 2 (Hash B)
      |                     |
      v (顺时针寻找最近节点)   v
    Node 1                Key 2
        \                /
         \              /
             Node 3 (Hash C)
  1. 节点映射:使用服务器的 IP 或主机名计算哈希值,将其映射到环上的某个位置;
  2. 数据映射:计算数据 Key 的哈希值,映射到环上;
  3. 寻址规则:从 Key 的位置出发,沿顺时针方向寻找遇到的第一个服务器节点,该节点即为数据存储目标;
  4. 扩缩容影响最小化:当新增或删除一个节点时,仅影响该节点与其在环上逆时针方向相邻节点之间的一小段数据,其余所有节点的数据映射完全不受影响!

虚拟节点(Virtual Nodes)消除数据倾斜

若真实物理节点较少,物理节点在环上的分布极易出现不均匀(数据倾斜 Data Skewness)。

  • 解决方案:为每台物理节点引入数百个“虚拟节点”(例如物理机 Node_A 对应虚拟节点 Node_A#1, Node_A#2, ..., Node_A#200);
  • 效果:环上的虚拟节点分布极其均匀,各物理机器负载均衡,且节点宕机时其负载会均匀分摊给其余所有物理机器,避免局部过载。

2. 缓存设计与高可用模式 (Caching)

内存缓存(如 Redis, Memcached)的读取延迟通常在亚毫秒级(\(< 1\text{ ms}\)),比磁盘数据库快 \(100 \sim 1000\) 倍。

2.1 常见缓存读写模式

1. Cache-Aside(旁路缓存模式,最经典主流)

由业务应用代码直接维护缓存与数据库的关系: - 读取数据:先查缓存;若命中则直接返回;若未命中则查询数据库,将结果写入缓存,最后返回。 - 写入数据:先更新数据库,再删除缓存(Cache Invalidation)。

为什么是“先写库再删缓存”,而不是“先删缓存再写库”或“更新缓存”?
- 若先删缓存再写库:在高并发下,线程 A 删了缓存还没写完库,线程 B 读取未命中去查库读到旧数据并写回缓存,导致脏数据永久滞留缓存!
- 若更新缓存:如果写入频繁而读取很少,每次写入都去计算并更新缓存属于白白浪费性能;且在并发双写时可能发生覆盖顺序颠倒(线程 A 先写库后更新缓存,线程 B 后写库先更新缓存,导致缓存为 A 的旧值)。

2. Write-Through(直写模式)

应用将缓存视为权威存储源,所有读写均直接面向缓存层。写入时由缓存组件同步将数据更新到后端数据库,保证绝对强一致,但写延迟较高。

3. Write-Back / Write-Behind(异步回写模式)

应用只写入缓存并立即返回成功,由缓存系统在后台异步批量刷入数据库。 - 优点:极高的写吞吐量与极低写延迟; - 缺点:若缓存节点在数据刷盘前突然宕机,存在数据永久丢失的风险(适用于日志收集、实时点赞量统计等高容忍场景)。


2.2 缓存三大灾难全方位防御

graph TD
    subgraph "缓存穿透 (Cache Penetration)"
        A1["查询根本不存在的 Key"] --> A2["缓存查不到 -> 穿透直击数据库"]
        A2 --> A3["防御: 布隆过滤器 (Bloom Filter) 拦截<br>+ 缓存空值 (设置短过期时间)"]
    end

    subgraph "缓存击穿 (Cache Breakdown)"
        B1["单一超高热点 Key 突然过期"] --> B2["瞬间海量并发同时打向数据库"]
        B2 --> B3["防御: 互斥锁 (Mutex / SETNX) 仅允许一线程查库<br>+ 逻辑不过期 (后台异步刷新)"]
    end

    subgraph "缓存雪崩 (Cache Avalanche)"
        C1["大量 Key 集中同一秒过期<br>或 Redis 节点集体宕机"] --> C2["数据库瞬间被千万级流量压垮"]
        C3["防御: 过期时间增加随机抖动 Jitter<br>+ 多级缓存 (Local Cache) + Redis Cluster 高可用"]
    end

3. 数据库扩展与架构演进 (Database Scaling)

单机关系型数据库通常受限于物理机 CPU、内存与磁盘 I/O 吞吐上限(一般并发连接数在数千以内)。系统演进必须遵循横向扩展路径。

3.1 读写分离与主从复制 (Read/Write Splitting)

  • 主库(Primary / Master):专门处理所有 INSERT / UPDATE / DELETE 写请求,将变更记录到二进制日志(binlog);
  • 从库(Replica / Slave):通过异步或半同步复制回放 binlog,专门分担 SELECT 读请求。

主从复制延迟问题与应对:
从库同步存在毫秒至秒级的延迟。如果用户刚发了一条动态立刻刷新页面,读请求路由到从库可能读不到刚发的内容(违反“读写一致性 Read-Your-Writes”)。
解决方案:
1. 关键业务强制路由主库:对刚完成写操作的用户,其在接下来 2 秒内的相关读请求强制走主库;
2. 携带版本号/时间戳:读请求带上写入时的时间戳,从库若同步位点落后于该时间戳则等待或转查主库。


3.2 分库分表 (Sharding)

当单表数据量超过千万级、B+ 树深度增加至 4 层以上,或者数据总容量超过单机磁盘上限时,必须进行分库分表。

1. 垂直拆分 (Vertical Partitioning)

  • 垂直分库:按业务边界拆分(如用户库、商品库、订单库、支付库微服务化);
  • 垂直分表:将大表中不常用或大文本字段(如文章正文、商品详情 text)拆分到扩展表中,提高主表单页容纳行数与索引扫描效率。

2. 水平分片 (Horizontal Sharding)

将同一张表的不同行分散到不同的物理数据库/表中: - 范围分片(Range-based):按时间(如每月一张表)或 ID 范围(1-1000万,1000万-2000万)。优点是易于扩展;缺点是写入流量集中在最新分片,产生明显写热点。 - 哈希分片(Hash-based):按用户 ID 计算 hash(user_id) % 分片数。优点是数据和写入极其均匀;缺点是未来二次扩容重新再平衡(Re-sharding)成本极高。

分库分表带来的架构挑战与代价:

  1. 跨分片事务:单机 ACID 事务失效,必须引入 2PC、Saga 模式或基于消息队列的最终一致性事务方案;
  2. 跨分片 JOIN:关系型数据库不再支持跨分片关联,必须在应用层做多次查询聚合内存拼装;
  3. 全局唯一主键:不能再依赖单表自增 ID,必须引入分布式 ID 发号器。

3.3 存储引擎对比:B+ Tree vs LSM-Tree

维度 B+ Tree(如 MySQL InnoDB) LSM-Tree(如 RocksDB, Cassandra, LevelDB)
数据组织结构 多叉平衡平衡树,叶子节点构成双向链表,内部节点仅存索引。 内存中的 MemTable(跳表) + 磁盘顺序追加的 WAL 日志 + 分层不可变的 SSTable 文件。
写入性能 较慢(涉及随机 I/O,为了更新树节点可能触发页分裂与就地覆盖写 In-place Update)。 极快(纯顺序追加写 Sequential I/O),所有写入先入内存与追加写日志,后台异步进行 Compaction 归并。
读取性能 极快(点查与范围查询稳定且极高),通常仅需 3 次左右磁盘 I/O。 较慢(点查可能需要使用布隆过滤器过滤,并在多层 SSTable 中检索合并,存在读放大)。
写放大 (Write Amplification) 较严重(修改 1 个字节也必须将整个 16KB 数据页刷盘)。 存在于后台分层压缩合并阶段,但单次写入前端完全是顺序追加。
适用场景 读多写少、事务一致性要求极高的 OLTP 核心系统。 写多读少、海量写入高吞吐(如时序监控日志、即时通讯聊天记录、大数据摄入)。

4. 消息队列与异步解耦 (Message Queues)

现代高并发架构中,消息队列(如 Apache Kafka, RabbitMQ, RocketMQ)主要发挥三大核心价值: 1. 异步解耦:将非核心流程(如下单后的发送短信、赠送积分、通知仓储)从主链路剥离,主接口直接响应,大幅降低响应延迟; 2. 削峰填谷:在秒杀大促等瞬时流量高峰期,消息队列充当蓄水池吸收百万级请求,后端处理服务按自身处理能力恒定速率平稳消费,防止下游系统崩溃; 3. 最终一致性与可靠投递:保证分布式服务之间的数据可靠流转。


5. 分布式唯一 ID 生成器 (Distributed Unique IDs)

在分库分表与多中心多活架构下,必须生成全网唯一的 64 位整数 ID。

5.1 Twitter 雪花算法 (Snowflake)

雪花算法生成一个全局唯一的 64 位有符号整数,其二进制结构如下:

+-------------------------------------------------------------------------+
| 1 bit |        41 bits        |      10 bits     |        12 bits       |
| 符号位 |       时间戳毫秒差值    |   工作机器/数据中心ID |        自增序列号    |
+-------------------------------------------------------------------------+
  1. 1 bit 符号位:恒为 0,保证生成的数字始终为正数;
  2. 41 bit 时间戳:记录相对于自定义纪元(Epoch)的毫秒数。\(2^{41}\) 毫秒约合 69 年;
  3. 10 bit 机器 ID:支持配置多达 \(2^{10} = 1024\) 个独立的物理工作节点;
  4. 12 bit 序列号:单台机器在同一毫秒内可递增生成 \(2^{12} = 4096\) 个不重复 ID。

  5. 优势:纯内存本地生成,性能极高(单机可达 \(400\) 万 QPS);全局趋势递增,非常契合 InnoDB B+ 树主键索引的顺序写入,杜绝页分裂。

  6. 致命缺陷:时钟回拨(Clock Move Backwards):若服务器 NTP 校时发生时间向后跳跃,会导致生成与过去重复的 ID。
  7. 防御策略:检测到当前时间小于上次记录时间,若回拨毫秒较小(如 \(< 5\text{ ms}\))则自旋等待;若回拨过大则报警切流或使用备用机房 ID。

6. 分布式锁与并发控制 (Distributed Locks)

在跨多个进程/节点的微服务环境中,本地语言锁(如 synchronized、std::mutex)完全失效,必须依赖分布式锁保证互斥性。

6.1 基于 Redis 的分布式锁实现

最标准且健壮的 Redis 分布式锁必须遵循以下设计:

-- 加锁: 使用 SET resource_name my_random_value NX PX 30000
-- 释放锁必须使用 Lua 脚本保证“校验拥有者 + 删除”原子执行
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  1. 互斥性:NX 参数确保仅当 Key 不存在时才能设置成功;
  2. 防死锁:PX 设置过期时间,即使持有锁的进程崩溃重启,锁到期后自动释放;
  3. 防误删(安全归属):Value 必须存入具有唯一性的客户端凭据(如 UUID + 线程 ID)。防止线程 A 执行超时锁被自动释放后,线程 B 获取锁,随后线程 A 执行完毕误将线程 B 的锁强行删除!
  4. 看门狗自动续期(Watchdog):业务执行时间可能超过锁过期时间。成熟的工业级框架(如 Java Redisson)会在后台启动守护线程,每隔一段时间(如过期时间的 1/3)自动延长锁的 TTL。