2. 缓存基础:本地缓存与分布式缓存原理、缓存穿透/击穿/雪崩

缓存是后端系统应对高并发、提升响应速度的核心手段。本章从缓存分类出发,深入剖析本地缓存与分布式缓存的实现原理,并重点讲解三种经典缓存故障——穿透、击穿、雪崩的成因与解决方案。理解这些基础,是后续构建缓存一致性闭环与可观测性整合的前提。

2.1 缓存分类与核心原理

根据数据存储位置与共享范围,缓存可分为本地缓存和分布式缓存两大类。两者在性能、一致性、容量和适用场景上存在显著差异。

对比维度 本地缓存 分布式缓存
存储位置 应用进程内(JVM 堆 / 堆外内存) 独立缓存服务(Redis / Memcached 集群)
访问延迟 纳秒级(无网络开销) 微秒~毫秒级(网络 RTT)
容量上限 受限于单机内存(通常 GB 级) 可水平扩展至 TB 级
数据一致性 各节点独立,易出现不一致 中心化存储,一致性相对可控
典型实现 Caffeine, Guava Cache, Ehcache Redis, Memcached, Hazelcast
适用场景 配置信息、热点数据、单机计算缓存 全局共享数据、会话状态、跨服务缓存
架构决策要点: 本地缓存适合延迟敏感、数据变更频率低的场景;分布式缓存适合跨进程共享、需要全局一致性的场景。生产环境中常采用多级缓存架构——本地缓存兜底热点,分布式缓存承载全量。

2.2 本地缓存实现原理

本地缓存本质是一个内存中的键值存储,核心组件包括:存储引擎、淘汰策略、过期机制、并发控制。

  • 存储引擎: 通常使用 ConcurrentHashMap 或类似结构,支持高并发读写。
  • 淘汰策略: 当缓存容量达到上限时,按策略移除旧数据。常见策略包括:
    • LRU(最近最少使用): 淘汰最久未被访问的条目。
    • LFU(最不经常使用): 淘汰访问频率最低的条目。
    • FIFO(先进先出): 按写入顺序淘汰。
    • TTL(过期时间): 基于写入时间或最后访问时间自动失效。
  • 并发控制: 通过分段锁、读写锁或 CAS 操作保证线程安全。
// Caffeine 本地缓存典型配置示例
Cache<String, UserProfile> cache = Caffeine.newBuilder()
    .maximumSize(10_000)               // 最大条目数
    .expireAfterWrite(5, TimeUnit.MINUTES) // 写入后过期
    .recordStats()                     // 开启统计,用于可观测性
    .build();

// 缓存穿透保护:结合 LoadingCache 自动加载
LoadingCache<String, UserProfile> loadingCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .build(key -> userService.loadFromDB(key));

2.3 分布式缓存:Redis 与 Memcached

分布式缓存将数据分散存储在集群节点上,对外提供统一的读写接口。Redis 和 Memcached 是当前最主流的两种方案。

2.3.1 Redis 核心特性

  • 丰富的数据结构: String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Stream。
  • 持久化: RDB(快照)和 AOF(追加日志),支持数据恢复。
  • 高可用: 主从复制 + Sentinel 自动故障转移;Redis Cluster 提供分片与自动重分片。
  • 原子操作: 支持 Lua 脚本、事务、分布式锁(SETNX + Redlock)。
  • 过期策略: 惰性删除 + 定期扫描,支持精确 TTL。

2.3.2 Memcached 核心特性

  • 纯内存缓存: 不支持持久化,重启后数据全部丢失。
  • 简单 KV 模型: 仅支持 String 类型,value 最大 1MB。
  • 多线程架构: 利用多核 CPU,读写性能极高。
  • LRU 淘汰: 内存满时自动淘汰最近最少使用的条目。
  • 客户端分片: 一致性哈希由客户端实现,服务端无集群管理。
选型建议: 需要数据结构灵活性、持久化、高可用时选择 Redis;追求极致读写性能、数据可丢失、仅需简单 KV 时选择 Memcached。当前业界 Redis 占据绝对主导地位。

2.4 缓存穿透、击穿与雪崩

这三种故障是缓存系统最常见的稳定性杀手,理解其成因与应对方案是架构师的必修课。

核心区别速记: 穿透是“查不到”导致绕过缓存直击 DB;击穿是“热点 key 失效”导致并发打穿;雪崩是“大量 key 同时失效”导致 DB 被压垮。

2.4.1 缓存穿透

定义: 查询一个根本不存在的数据,缓存和数据库都查不到,每次请求都直接打到数据库。

常见原因:

  • 恶意攻击:遍历不存在的 ID。
  • 业务漏洞:查询条件错误导致 key 不存在。

解决方案:

  • 缓存空值: 将 null 结果也缓存,设置较短 TTL(如 30~60 秒)。
  • 布隆过滤器: 在缓存前加一层 Bloom Filter,快速判断 key 是否可能存在。
  • 参数校验: 在入口层拦截非法参数。

2.4.2 缓存击穿

定义: 一个热点 key 在缓存失效的瞬间,大量并发请求同时穿透到数据库,导致 DB 负载飙升。

解决方案:

  • 互斥锁: 只允许一个线程去加载数据,其他线程等待或快速失败。
  • 逻辑过期: 缓存永不过期,但 value 中携带过期时间,异步刷新。
  • 热点数据预热: 系统启动时主动加载热点 key,避免冷启动击穿。
// 互斥锁防止缓存击穿(Redis + Redisson 示例)
public String getWithMutex(String key) {
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    // 尝试获取分布式锁
    String lockKey = "lock:" + key;
    RLock lock = redissonClient.getLock(lockKey);
    try {
        if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {
            // 双重检查:锁获取后再次查询缓存
            value = redis.get(key);
            if (value != null) {
                return value;
            }
            // 从数据库加载
            value = db.query(key);
            redis.setex(key, 300, value);
            return value;
        } else {
            // 等待后重试或返回降级数据
            Thread.sleep(50);
            return getWithMutex(key);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return fallback(key);
    } finally {
        lock.unlock();
    }
}

2.4.3 缓存雪崩

定义: 大量缓存 key 在同一时间集中过期,或缓存节点宕机,导致所有请求直接打到数据库,引发连锁崩溃。

解决方案:

  • 过期时间随机化: 在基础 TTL 上增加随机偏移(如 ±10%~30%),避免集体失效。
  • 多级缓存: 本地缓存 + 分布式缓存组合,本地缓存作为最后一道防线。
  • 缓存高可用: Redis Sentinel/Cluster 自动故障转移,避免单点。
  • 限流降级: 对 DB 访问做限流,超出阈值时返回降级数据或错误提示。
  • 提前预热: 大促前主动加载热点数据,分散过期时间。

2.5 三种故障对比与应对矩阵

故障类型 根本原因 影响范围 核心应对策略
缓存穿透 查询不存在的数据 单个 key 持续打 DB 缓存空值、布隆过滤器、参数校验
缓存击穿 热点 key 失效瞬间并发 单个热点 key 瞬时打 DB 互斥锁、逻辑过期、热点预热
缓存雪崩 大量 key 同时过期 / 节点宕机 全局流量打 DB,可能雪崩 TTL 随机化、多级缓存、高可用、限流降级
缓存穿透 / 击穿 / 雪崩 故障链路与应对策略 客户端请求 缓存层(Redis / 本地缓存) 穿透:key 不存在 击穿:热点 key 失效 雪崩:大量 key 过期 数据库(DB) 应对策略:缓存空值 / 布隆过滤器(穿透) | 互斥锁 / 逻辑过期(击穿) | TTL 随机化 / 多级缓存 / 限流降级(雪崩)

2.6 本章小结