3. 缓存基础:缓存的作用与类型、常见策略及经典问题

在分布式系统中,缓存是提升读取性能、降低后端负载的核心手段。对于 gRPC 服务而言,合理的缓存策略能显著减少重复计算与网络 I/O,但同时也引入了数据一致性的挑战。本节将系统梳理缓存的基础知识,为后续讨论 gRPC 缓存一致性方案奠定理论基础。

3.1 缓存的核心作用

缓存通过将热点数据存储在高速存储介质中,缩短数据访问路径。其核心收益体现在以下三个维度:

维度 说明 典型效果
降低延迟 内存访问延迟通常在微秒级,远低于磁盘 I/O(毫秒级)或远程 RPC(数毫秒至数十毫秒) 读延迟从 10ms 降至 1ms 以内
减轻后端压力 缓存吸收大量重复读请求,减少对数据库或下游服务的冲击 数据库 QPS 降低 60%~90%
提升吞吐量 缓存层可承载更高的并发读请求,突破后端瓶颈 系统吞吐量提升 5~10 倍
实践提示:在 gRPC 服务中,缓存通常作用于响应结果(如用户信息、配置数据)或中间计算结果(如聚合查询)。对于写密集型场景,需谨慎评估缓存带来的一致性问题。

3.2 缓存类型:本地缓存 vs 分布式缓存

根据缓存数据的存储位置与共享范围,缓存可分为本地缓存与分布式缓存两大类。两者在架构、性能与一致性模型上存在本质差异。

3.2.1 本地缓存

本地缓存将数据存储在单个进程的内存中,所有数据仅对当前进程可见。常见实现包括 Guava Cache、Caffeine、Ehcache 以及 Go 中的 sync.Map。

  • 优势:访问速度极快(无网络开销),实现简单,适合单机场景。
  • 劣势:数据无法跨进程共享,每个节点各自维护一份副本,导致内存浪费与数据不一致。
  • 典型应用:配置信息、静态字典、不频繁变更的元数据。

3.2.2 分布式缓存

分布式缓存将数据存储在独立的缓存集群中,所有服务节点共享同一份缓存数据。典型代表为 Redis、Memcached。

  • 优势:数据全局共享,容量可水平扩展,支持丰富的数据结构与过期策略。
  • 劣势:引入网络延迟(通常 1~5ms),依赖缓存中间件的稳定性。
  • 典型应用:用户会话、商品详情、热点排行榜。
选型对比:
对比维度 本地缓存 分布式缓存
访问延迟 纳秒~微秒级 毫秒级(含网络 RTT)
数据一致性 节点间天然不一致 全局一致(需配合一致性协议)
容量上限 受限于单机内存 可扩展至 TB 级
故障影响 仅影响本节点 可能影响整个集群
gRPC 适用场景 无状态服务内临时缓存 跨服务共享数据缓存
缓存架构对比:本地缓存 vs 分布式缓存 本地缓存架构 服务节点 A 本地缓存 (Caffeine) 服务节点 B 本地缓存 (Caffeine) 共享数据库 ⚠ 节点间缓存数据可能不一致 各自独立过期,无协调机制 分布式缓存架构 服务节点 C gRPC 客户端 服务节点 D gRPC 客户端 Redis 缓存集群 全局共享数据 ✅ 所有节点读取同一份缓存 数据全局一致(需处理网络分区)

3.3 常见缓存策略

缓存策略决定了数据何时被淘汰、如何保持新鲜度。以下三种策略是工程实践中最常用的基础策略。

3.3.1 LRU(Least Recently Used)

LRU 淘汰最长时间未被访问的数据。其核心假设是:最近被访问过的数据,未来被访问的概率更高。

  • 实现方式:双向链表 + 哈希表(如 LinkedHashMap)。
  • 优点:对热点数据敏感,能自动适应访问模式变化。
  • 缺点:偶发性的全表扫描可能导致大量冷数据被误认为热点。
  • 适用场景:通用缓存,如 Caffeine 默认策略。

3.3.2 TTL(Time To Live)

TTL 为每个缓存条目设置一个固定的生存时间,到期后自动失效。这是最直观的过期策略。

  • 实现方式:在存储时记录过期时间戳,读取时检查是否过期。
  • 优点:实现简单,能保证数据最终一致性。
  • 缺点:TTL 过短会导致缓存命中率低;TTL 过长则数据陈旧。
  • 适用场景:验证码、会话 Token、配置信息。

3.3.3 LFU(Least Frequently Used)

LFU 淘汰访问频率最低的数据。它通过统计访问次数来识别真正的热点。

  • 实现方式:最小堆或计数数组,维护每个 key 的访问频次。
  • 优点:能有效抵抗突发流量对缓存的污染。
  • 缺点:需要维护访问计数,内存开销较大;对访问模式变化不敏感。
  • 适用场景:长期稳定的热点数据,如热门文章排行榜。
策略 淘汰依据 内存开销 热点适应性 典型实现
LRU 最近访问时间 低(仅维护链表) 高(快速响应变化) Caffeine、Redis 近似 LRU
TTL 固定时间窗口 极低 低(固定过期) Redis EXPIRE、Memcached
LFU 访问频次 中(需计数) 中(对突发不敏感) Redis LFU 模式、Caffeine

3.4 缓存经典问题:穿透、击穿与雪崩

这三个问题是缓存系统中最常见、破坏性最大的故障模式。理解其成因与解决方案,是构建高可用缓存系统的必修课。

3.4.1 缓存穿透

定义:查询一个根本不存在的数据,缓存层和数据库层都没有命中,导致每次请求都穿透到数据库。

  • 危害:恶意攻击者可利用不存在的 key 对数据库发起大量无效查询,导致数据库压力骤增甚至宕机。
  • 解决方案:
    • 缓存空值:将空结果也缓存起来,设置较短的 TTL(如 30~60 秒)。
    • 布隆过滤器:在缓存前加一层布隆过滤器,快速判断 key 是否可能存在。

3.4.2 缓存击穿

定义:一个热点 key 在缓存过期的瞬间,大量并发请求同时涌入,直接打到数据库。

  • 危害:单个热点 key 的失效可能导致数据库瞬间承受数万 QPS 的冲击。
  • 解决方案:
    • 互斥锁:只允许一个线程去加载数据,其他线程等待或返回旧值。
    • 逻辑过期:缓存永不过期,但存储逻辑过期时间,异步刷新。

3.4.3 缓存雪崩

定义:大量缓存 key 在同一时间过期,或缓存节点整体宕机,导致所有请求直接压向数据库。

  • 危害:数据库可能被瞬间击垮,引发级联故障,导致整个服务不可用。
  • 解决方案:
    • 过期时间随机化:在基础 TTL 上增加随机偏移量(如 ±10%~30%)。
    • 缓存高可用:使用 Redis 哨兵或集群模式,避免单点故障。
    • 限流降级:在数据库层设置连接池上限,保护后端。
缓存三大经典问题对比 缓存穿透 请求不存在的 key 缓存未命中 数据库未命中 方案:缓存空值 或布隆过滤器 缓存击穿 热点 key 过期 大量并发请求 数据库压力暴增 方案:互斥锁 或逻辑过期 缓存雪崩 大量 key 同时过期 缓存层整体失效 数据库被击垮 方案:TTL 随机化 + 限流降级
关键提醒:在 gRPC 微服务架构中,缓存穿透和击穿往往被放大。因为一个 gRPC 服务可能被上游数十个服务调用,单个 key 的失效可能引发“调用链雪崩”。建议在 gRPC 拦截器层统一实现缓存防护逻辑,而不是在每个业务方法中重复处理。

3.5 缓存策略组合实践

在实际工程中,单一策略往往无法满足所有需求。常见的组合模式包括:

  • LRU + TTL:Caffeine 默认采用此组合,既保证热点数据留存,又确保数据最终过期。
  • LFU + TTL:Redis 的 allkeys-lfu 策略配合 EXPIRE 命令,适合长期稳定的热点场景。
  • 本地缓存 + 分布式缓存:多级缓存架构,本地缓存扛高频读,分布式缓存做数据同步与兜底。
gRPC 场景推荐:对于 gRPC 服务,建议采用“本地缓存(Caffeine/LRU+TTL)作为 L1 + Redis 分布式缓存作为 L2”的二级缓存架构。L1 负责扛住 80% 的重复读请求,L2 负责跨服务数据共享与一致性保障。后续章节将详细讨论这种架构下的一致性维护方案。