核心基础设施与分布式组件 (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\) 台服务器是一个经典难题。
传统取模哈希的灾难¶
若采用传统哈希公式:
当集群扩容(\(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)
- 节点映射:使用服务器的 IP 或主机名计算哈希值,将其映射到环上的某个位置;
- 数据映射:计算数据 Key 的哈希值,映射到环上;
- 寻址规则:从 Key 的位置出发,沿顺时针方向寻找遇到的第一个服务器节点,该节点即为数据存储目标;
- 扩缩容影响最小化:当新增或删除一个节点时,仅影响该节点与其在环上逆时针方向相邻节点之间的一小段数据,其余所有节点的数据映射完全不受影响!
虚拟节点(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)成本极高。
分库分表带来的架构挑战与代价:¶
- 跨分片事务:单机 ACID 事务失效,必须引入 2PC、Saga 模式或基于消息队列的最终一致性事务方案;
- 跨分片 JOIN:关系型数据库不再支持跨分片关联,必须在应用层做多次查询聚合内存拼装;
- 全局唯一主键:不能再依赖单表自增 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 bit 符号位:恒为
0,保证生成的数字始终为正数; - 41 bit 时间戳:记录相对于自定义纪元(Epoch)的毫秒数。\(2^{41}\) 毫秒约合 69 年;
- 10 bit 机器 ID:支持配置多达 \(2^{10} = 1024\) 个独立的物理工作节点;
-
12 bit 序列号:单台机器在同一毫秒内可递增生成 \(2^{12} = 4096\) 个不重复 ID。
-
优势:纯内存本地生成,性能极高(单机可达 \(400\) 万 QPS);全局趋势递增,非常契合 InnoDB B+ 树主键索引的顺序写入,杜绝页分裂。
- 致命缺陷:时钟回拨(Clock Move Backwards):若服务器 NTP 校时发生时间向后跳跃,会导致生成与过去重复的 ID。
- 防御策略:检测到当前时间小于上次记录时间,若回拨毫秒较小(如 \(< 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
- 互斥性:
NX参数确保仅当 Key 不存在时才能设置成功; - 防死锁:
PX设置过期时间,即使持有锁的进程崩溃重启,锁到期后自动释放; - 防误删(安全归属):Value 必须存入具有唯一性的客户端凭据(如 UUID + 线程 ID)。防止线程 A 执行超时锁被自动释放后,线程 B 获取锁,随后线程 A 执行完毕误将线程 B 的锁强行删除!
- 看门狗自动续期(Watchdog):业务执行时间可能超过锁过期时间。成熟的工业级框架(如 Java Redisson)会在后台启动守护线程,每隔一段时间(如过期时间的 1/3)自动延长锁的 TTL。