设计一个分布式缓存系统

本文系统设计一个高可用分布式缓存系统:需求澄清与量级估算、缓存架构(分片/一致性哈希/代理层)、缓存与数据库一致性(Cache Aside/双删/Binlog 订阅)、高可用与容灾(主从/哨兵/集群)、热点治理与穿透/雪崩防护,并给出架构图、路由伪代码与一致性方案对比。

缓存是系统性能的第一杠杆:同样的数据,从内存读是微秒级,从数据库读是毫秒级,从远端读是十毫秒级——差着两个数量级。分布式缓存系统把「热数据」从数据库搬到多台机器的内存里,用一致性哈希路由、多副本容灾、与数据库的双写协同,为上层业务提供低延迟、高并发、高可用的读服务。本文按照系统设计面试的标准答题结构,设计一个生产级的分布式缓存系统。

一句话:分布式缓存的核心矛盾是「快」与「一致」——设计上把「多读少写、可容忍短暂不一致」的数据放进缓存,用一致性哈希找节点、用双写策略保最终一致、用多副本保可用。

一、需求澄清与量级估算

1.1 需求澄清

面试官给出题目「设计一个分布式缓存系统」后,先通过提问明确边界:

  • 数据模型:KV 缓存(字符串/哈希/列表),还是更丰富的结构?是否需要排序、计数?
  • 一致性要求:缓存与数据库能否短暂不一致?能否容忍旧数据?
  • 容量规模:总缓存容量、单 key 大小、平均/峰值 QPS?
  • 过期策略:是否支持 TTL?淘汰策略(LRU/LFU)?
  • 高可用:宕机如何恢复?是否需要持久化?
  • 访问模式:读多写少还是写读均衡?是否有热点 key?
  • 使用方式:业务直接连还是经过代理层?

明确假设(面向面试的合理假设):

需求项假设
数据模型KV + 常用数据结构(hash/list/set/zset)
一致性缓存最终一致,允许秒级短暂不一致
规模1000 节点、总容量 10 TB、峰值 1000 万 QPS
过期支持 TTL + LRU/LFU 淘汰
高可用主从 + 自动故障转移,可重启恢复
访问读多写少,偶发热点 key

1.2 量级估算

指标估算值推导
节点数1000 台每台 16G 内存,共 10~16 TB
峰值 QPS1000 万缓存命中为主,读放大后打到 DB 的应被滤掉
平均时延P99 < 1ms同机房内存访问
缓存命中率95%+读多写少的典型曲线
单 key 平均~1KB用户/商品/会话等对象
写 QPS100 万数据更新/失效
热点 key少数 key 占 30% 流量爆款商品、热搜词

一句话:1000 万 QPS 全打在内存上才现实——缓存系统的一切设计(分片、副本、代理、命中率)最终都是为了「把流量留在内存、把数据库保护起来」。

二、高层架构设计

   ┌──────────────────────────┐
   │       业务服务集群         │
   │  (读路径 / 写路径)        │
   └────────────┬─────────────┘
                │ 缓存读写请求
                ▼
   ┌─────────────────────────────────────────────────────────┐
   │             缓存代理层 (Cache Proxy, 无状态)               │
   │  ① 一致性哈希路由 ② 连接复用/协议解析 ③ 请求合并         │
   │  ④ 慢查询/热 key 感知 ⑤ 故障节点摘除                    │
   └────────────┬────────────────────────────────────────────┘
                │ 一致性哈希定位到分片
                ▼
   ┌─────────────────────────────────────────────────────────┐
   │              缓存数据层 (分片集群)                        │
   │  ┌──────────┐ ┌──────────┐ ┌──────────┐  ┌──────────┐    │
   │  │ Shard 0  │ │ Shard 1  │ │ Shard 2  │… │ Shard N  │    │
   │  │ 主+从    │ │ 主+从    │ │ 主+从    │  │ 主+从    │    │
   │  └──────────┘ └──────────┘ └──────────┘  └──────────┘    │
   └────────────┬────────────────────────────────────────────┘
                │ 数据同步/故障转移
                ▼
   ┌─────────────────────────────────────────────────────────┐
   │  管控面:元数据(分片映射/路由表) 监控(命中率/延迟/容量)    │
   │  一致性协同:DB Binlog 订阅 → 失效/回写缓存               │
   └─────────────────────────────────────────────────────────┘

整体拆为四层:

  1. 代理层:无状态入口,一致性哈希路由、连接复用、故障摘除、指标采集。
  2. 数据层:按分片组织的缓存节点,主从结构,每分片独立。
  3. 管控面:分片映射与路由表下发、监控告警、扩缩容编排。
  4. 协同面:与数据库的一致性协同(Binlog 订阅失效/回写)。

2.1 一致性哈希路由

缓存集群扩容缩容频繁,路由算法要让「加减节点时影响面最小」:

一致性哈希:
  把 key 哈希到 0~2^32 环上,节点也哈希到环上
  key 顺时针找到的第一个节点即为归属节点
  加节点:只影响该节点逆时针到前一个节点之间的 key
  减节点:该节点的 key 迁移到下一个节点

虚拟节点:
  每个物理节点映射 100~200 个虚拟节点,打散到环上
  解决「节点少时数据倾斜」与「节点下线时负载不均衡」

数据迁移:
  加/减节点 → 重算受影响 key → 新节点缺失的 key 回源 DB 重建
  路由表版本化下发,代理层原子切换
;; 伪代码:一致性哈希路由
(defn route-key [key ring]
  (let [h (hash key)]
    ;; 环上找第一个 >= h 的虚拟节点
    (->> (sorted-map ring)  ; 环:虚拟节点哈希 → 物理节点
         (first-key-after h)
         (physical-node))))

;; 扩容:新节点上线后,只迁移落入新区间的 key

一句话:一致性哈希 + 虚拟节点让「扩缩容的扰动」从 O(N) 降到 O(1/N)——绝大多数 key 的路由不动,只有相邻区间的 key 需要迁移,这是缓存集群能平滑扩容的数学保证。

三、核心组件设计

3.1 缓存与数据库一致性

缓存与数据库是两套数据,任何双写都可能不一致。主流方案:

Cache Aside(旁路缓存)——最常用:
  读:先查缓存 → 未命中则查 DB → 回填缓存 → 返回
  写:先写 DB → 再删缓存(不是更新缓存)

为什么「写后删缓存」而非「写后更新缓存」:
  更新缓存是「写两处」,并发下容易写乱(后写覆盖先写)
  删缓存让「下次读」回源重建,天然收敛到 DB 最新值

潜在不一致窗口:
  读未命中 → 查 DB(旧) → 回填
  写 DB(新) → 删缓存
  读回填了旧值 → 缓存短暂旧 → 靠 TTL 或二次删兜底
;; 伪代码:Cache Aside 读
(defn get-cached [key]
  (if-let [v (cache/get key)]
    v
    (let [v (db/get key)]
      (cache/set key v :ttl 300)      ; 回填 + TTL
      v)))

;; 伪代码:Cache Aside 写(先 DB 后删缓存)
(defn update-user [user]
  (db/update user)
  (cache/del (str "user:" (:id user))))

结论:缓存一致性的工程共识是「Cache Aside + 写后删缓存」——它不追求无窗口,而是把窗口缩到 TTL 内并用「删缓存」让脏数据尽快被下一次读覆盖;追求更严时用「延迟双删」或 Binlog 订阅。

3.2 延迟双删与 Binlog 订阅

对一致性要求更高的场景,用两种增强手段:

延迟双删(先删再删):
  写 DB → 删缓存 → 等待约 100ms(给读回填一个完成窗口)→ 再删一次
  把「读回填旧值」这个窗口内的脏缓存二次清除

Binlog 订阅(最终一致最稳):
  写 DB → MySQL Binlog 捕获变更 → MQ → 消费者删缓存/重建缓存
  优点:业务代码不感知缓存,主数据变更统一驱动缓存
  缺点:多引入一套 Binlog 消费链路,延迟在秒级
选型:
  - 常规业务:Cache Aside + 删缓存 + TTL 兜底(够用)
  - 一致性敏感:延迟双删 或 Binlog 订阅
  - 强一致要求的数据:根本不该进缓存(走 DB)

3.3 高可用与容灾

缓存宕机会把流量打到数据库,必须多副本 + 自动转移:

主从复制:
  每分片 1 主 N 从,主写从读(读写分离)
  从节点异步复制主节点数据,读扩展 + 容灾

故障转移:
  哨兵/集群控制器监控主节点心跳
  主挂 → 提升从节点为新主 → 代理层路由表摘除故障主节点
  客户端重试 + 路由表更新,秒级恢复

持久化(可选):
  缓存本可丢(丢了回源重建),但为「快速恢复 + 防雪崩」可开 AOF
  取舍:AOF 保数据恢复速度,牺牲一点写性能

一句话:缓存的高可用靠「主从 + 自动转移 + 回源重建」三件套——即使整个分片挂了,丢失的 key 由 DB 重建,代价只是短暂命中率下降,系统不雪崩。

3.4 热点治理

少量 key 承载大量流量,单分片会成为瓶颈:

热点识别:
  代理层统计 key 访问频次 → 识别 topN 热点 key
  热点 key 下发到各业务节点本地缓存(JVM 缓存副本)

本地缓存副本:
  热点 key 在业务本地缓存一份(TTL 极短,如 1~5 秒)
  读命中本地 → 不经过缓存集群 → 分散热点压力
  写更新 → 删本地(广播或靠短 TTL 自然过期)

多副本缓存:
  把热点 key 复制到多个分片(key#1 / key#2 / ...)
  代理层按随机取副本,摊薄单分片压力
;; 伪代码:热点 key 本地缓存兜底
(defn get-with-local [key]
  (if (hot-key? key)
    (or (local-get key)          ; 命中本地副本
        (let [v (cache/get key)]
          (local-set key v :ttl 2)
          v))
    (cache/get key)))

要点:热点治理的三板斧是「识别 → 打散 → 兜底」——识别热点 key,把访问打散到业务本地缓存或多副本分片,DB 与缓存集群都不用硬扛单点。

四、深入权衡

4.1 缓存穿透、击穿与雪崩

故障现象根治方案
穿透查询不存在的 key,每次都打 DB布隆过滤器挡不存在 key + 空值缓存
击穿热点 key 过期瞬间,并发打 DB互斥锁重建 + 热点不过期
雪崩大量 key 同时过期/节点宕机,DB 被打垮过期时间加随机 + 多副本 + 限流降级
穿透防护:
  布隆过滤器(内存位图)判断 key 是否存在,不存在直接返回
  空值也缓存(TTL 短),防止恶意 key 反复穿透

击穿防护:
  热点 key 重建加分布式锁:只有一个线程回源 DB,其余等待
  逻辑过期:缓存里存「过期时间」,后台异步刷新,热 key 永不物理过期

雪崩防护:
  TTL 加随机抖动(300s ± 60s),避免集中失效
  缓存节点多副本,单点宕机不引起全局失效
  降级:缓存大量不可用时,DB 侧限流 + 熔断

结论:穿透、击穿、雪崩本质都是「DB 被不该打的流量打」——布隆过滤器挡穿透、互斥锁挡击穿、TTL 抖动与多副本挡雪崩,三套手段各管一摊,通常组合使用。

4.2 一致性 vs 可用性

方案一致性实现成本适用
只删缓存 + TTL最终一致,窗口秒级低大部分读多写少场景
延迟双删窗口缩到百毫秒中价格/库存等敏感读
Binlog 订阅回写最终一致,延迟秒级高主数据变更驱动的强协同
强一致(先更新缓存)无窗口但易写乱高几乎不用,违背缓存初衷

一句话:缓存本质是「用一致性换性能」——接受「短暂旧数据」换「低延迟高吞吐」是分布式缓存的契约;要强一致的数据就别进缓存,进了缓存就认最终一致。

4.3 命中率与容量

缓存命中率决定数据库压力,容量决定成本:

命中率提升:
  - TTL 设置合理(过短命中率低,过长脏数据久)
  - 热点提前预热(开服前回填热 key)
  - 淘汰策略匹配访问模式:读多写少用 LRU,稳定热门用 LFU

容量规划:
  容量 = 峰值读请求 × 单 key 大小 × 保留时长
  超容量 → 淘汰(LRU 驱逐)或加节点
  监控淘汰率:淘汰率突增说明容量不足,需扩容

五、总结

分布式缓存系统的骨架是「分片 + 副本 + 代理 + 一致性协同」:一致性哈希与虚拟节点把 key 均匀打散到分片并支撑平滑扩缩容,代理层做无状态路由、热点感知与故障摘除,主从结构与自动故障转移保证可用性,Cache Aside + 写后删缓存配合延迟双删或 Binlog 订阅实现最终一致。防护体系上,布隆过滤器挡穿透、互斥锁挡击穿、TTL 抖动与多副本挡雪崩、本地缓存与多副本打散热点。最终,缓存系统以「可容忍的短暂不一致」换来「百倍于数据库的读吞吐」,成为保护数据库、提升系统性能的第一道也是最重要的一道防线。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个 API 网关系统
  2. 设计一个短视频系统
  3. 设计一个日志检索系统