3. 分布式缓存基础:Redis核心数据结构与使用场景、缓存穿透/击穿/雪崩概念、缓存过期与淘汰策略

3.1 Redis核心数据结构与使用场景

Redis 作为分布式缓存的事实标准,其成功很大程度上归功于丰富且高效的数据结构。理解每种结构的底层实现与适用场景,是设计缓存策略的基础。

数据结构 底层编码 典型使用场景 时间复杂度(关键操作)
String int / embstr / raw 会话Token、计数器、分布式锁、对象JSON序列化缓存 GET/SET: O(1)
Hash ziplist / hashtable 对象字段级缓存(如用户信息)、购物车 HGET/HSET: O(1)
List quicklist 消息队列、最新消息列表、时间线 LPUSH/RPOP: O(1)
Set intset / hashtable 标签系统、好友关系、抽奖去重 SADD/SISMEMBER: O(1)
Sorted Set ziplist / skiplist 排行榜、延时队列、滑动窗口限流 ZADD: O(log N)
选型建议: 对于需要频繁修改对象中某个字段的场景(如更新用户最后登录时间),优先使用 Hash 而非整个 String 序列化,可显著减少网络开销与序列化成本。

3.2 缓存穿透、击穿与雪崩

这三个概念是分布式缓存设计中最常遇到的“三座大山”,其本质都是缓存失效导致请求直达后端存储,但触发原因与应对策略截然不同。

缓存三大异常:穿透 / 击穿 / 雪崩 ① 缓存穿透 查询不存在的数据 缓存与DB均无记录 解决方案 布隆过滤器 / 空值缓存 ② 缓存击穿 热点Key瞬间失效 高并发直击DB 解决方案 互斥锁 / 逻辑过期 ③ 缓存雪崩 大量Key同时失效 或Redis宕机 解决方案 过期时间加随机值 / 多级缓存 三者的核心区别 维度 穿透 击穿 雪崩 数据是否存在 不存在 存在(热点) 存在(批量) 影响范围 单次请求 单个Key 大量Key 核心防御 预校验 互斥 分散失效
生产警示: 缓存雪崩往往不是单一原因造成。Redis 实例宕机与大量 Key 同时过期叠加,会形成“雪崩+穿透”的复合故障。建议同时部署 Redis Sentinel/Cluster 高可用方案,并在客户端侧配置 本地缓存(如 Caffeine) 作为最后一道防线。

3.3 缓存过期与淘汰策略

Redis 的内存是有限的,当内存写满时,必须通过淘汰策略腾出空间。同时,过期 Key 的清理机制也直接影响缓存命中率与性能。

3.3.1 过期策略:被动 + 定期

  • 被动过期(惰性删除): 当客户端访问一个 Key 时,Redis 检查其是否过期,若过期则删除并返回 nil。优点是对 CPU 友好,缺点是过期 Key 可能长时间占用内存。
  • 定期过期(主动删除): Redis 每隔 100ms 随机抽取一批设置了过期时间的 Key,检查并删除已过期的 Key。通过 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 参数控制每次抽查数量。
最佳实践: 不要完全依赖 Redis 的过期清理。对于大量几乎同时过期的 Key(如每日凌晨失效的会话),建议在业务层为过期时间增加 随机偏移量(如基础 TTL + 随机 0~600 秒),避免集中过期导致 CPU 飙升与缓存雪崩。

3.3.2 内存淘汰策略(maxmemory-policy)

当 Redis 内存达到 maxmemory 上限时,将根据配置的策略选择要淘汰的 Key。不同策略对缓存命中率与系统性能影响差异巨大。

策略名称 淘汰范围 算法 适用场景
noeviction 不淘汰 — 写入失败返回错误,仅用于禁止淘汰的强一致性场景
allkeys-lru 所有 Key LRU(最近最少使用) 通用缓存,追求高命中率
volatile-lru 设置了 TTL 的 Key LRU 缓存与持久数据混合使用
allkeys-random 所有 Key 随机 数据访问频率均匀,无热点
volatile-random 设置了 TTL 的 Key 随机 同上,但仅淘汰带过期时间的 Key
volatile-ttl 设置了 TTL 的 Key TTL 最短优先 优先淘汰即将过期的数据
allkeys-lfu(4.0+) 所有 Key LFU(最不经常使用) 访问频率差异明显的场景
volatile-lfu(4.0+) 设置了 TTL 的 Key LFU 带过期时间的频率敏感场景
生产推荐: 绝大多数业务缓存场景,allkeys-lru 是最安全且高效的选择。它不关心 Key 是否设置过期时间,统一按访问热度淘汰,实现简单且命中率稳定。如果业务有明显的“冷热数据”分层(如热数据 1 小时、冷数据 1 天),可考虑 volatile-lru 配合精确的 TTL 设置。

3.3.3 淘汰策略选型决策流程

淘汰策略选型决策树 内存达到 maxmemory? 允许淘汰 访问频率差异大? 是 → allkeys-lfu | 否 → allkeys-lru 禁止淘汰 使用 noeviction 写入失败由业务层处理 核心原则:优先保证缓存命中率,其次考虑内存利用率

3.4 综合案例:缓存异常防御代码片段

以下代码展示了在业务层如何同时防御缓存穿透与击穿,使用 空值缓存 与 分布式互斥锁 组合策略。

public String getWithProtection(String key) {
    // 1. 查缓存
    String value = redis.get(key);
    if (value != null) {
        // 命中缓存,直接返回(包括空值标记)
        return value;
    }

    // 2. 缓存未命中,尝试获取分布式锁(防御击穿)
    String lockKey = "lock:" + key;
    boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
    if (!locked) {
        // 获取锁失败,说明其他线程正在加载,短暂等待后重试
        Thread.sleep(50);
        return redis.get(key); // 二次检查
    }

    try {
        // 3. 双重检查:获取锁后再次查询缓存
        value = redis.get(key);
        if (value != null) {
            return value;
        }

        // 4. 查询数据库
        value = db.query(key);
        if (value != null) {
            // 数据存在,写入缓存,TTL 随机化防雪崩
            int ttl = 3600 + new Random().nextInt(600);
            redis.setex(key, ttl, value);
        } else {
            // 数据不存在,写入空值缓存(防御穿透),TTL 较短
            redis.setex(key, 120, "NULL_VALUE");
        }
        return value;
    } finally {
        // 5. 释放锁
        redis.del(lockKey);
    }
}
注意: 上述代码中的 setnx 加锁方式仅用于演示核心逻辑。生产环境建议使用 Redisson 等成熟库的看门狗机制,避免锁超时导致并发问题。同时,空值缓存的 TTL 不宜过长,建议 1~5 分钟,并配合异步消息定期清理无效空值。