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 适用场景 | 无状态服务内临时缓存 | 跨服务共享数据缓存 |
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 哨兵或集群模式,避免单点故障。
- 限流降级:在数据库层设置连接池上限,保护后端。
关键提醒:在 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 负责跨服务数据共享与一致性保障。后续章节将详细讨论这种架构下的一致性维护方案。