4. 本地缓存 + Redis 两级缓存架构:Caffeine/Guava Cache 作为一级缓存、Redis 作为二级缓存、缓存一致性策略(TTL、主动失效)

4.1 两级缓存架构概述

在热点数据访问场景中,单一 Redis 缓存仍面临网络开销与序列化延迟。引入本地缓存(如 Caffeine、Guava Cache)作为一级缓存(L1),Redis 作为二级缓存(L2),形成两级缓存架构,可显著降低热点数据的访问延迟,减少 Redis 连接压力。

核心收益: 本地缓存命中时延 < 1ms(堆内访问),Redis 缓存命中时延约 1-5ms(网络+序列化),两级缓存整体吞吐量可提升 3-10 倍。
两级缓存架构数据流图 应用服务 L1 本地缓存 Caffeine / Guava Cache L2 Redis 缓存 分布式缓存集群 数据库 (DB) ① 读请求 ② 命中返回 ③ 未命中,查 L2 ④ 命中返回 ⑤ 未命中,查 DB ⑥ 回写 L2 ⑦ 回写 L1 说明: L1 缓存优先响应,L2 作为降级与共享层。L1 未命中时逐级下沉,数据回填时反向逐级写入。

4.2 一级缓存选型:Caffeine vs Guava Cache

对比维度 Caffeine Guava Cache
性能 基于 W-TinyLFU 算法,读写吞吐量更高 基于 LRU,高并发下锁竞争较明显
内存管理 支持基于频率和大小的驱逐策略,更精准 仅支持基于容量/时间的驱逐
异步加载 原生支持异步 LoadingCache 需手动封装
统计监控 内置 hit rate、miss rate、eviction count 基础统计,功能较弱
推荐场景 高并发、热点频繁变动的业务 低并发、简单缓存需求
选型建议: 新项目优先选择 Caffeine,其性能在 JDK 8+ 环境下比 Guava Cache 提升约 30%-50%。若已有 Guava 依赖且并发量不高,可继续使用。

4.3 两级缓存一致性策略

两级缓存的核心挑战是 数据一致性。本地缓存与 Redis 之间、Redis 与数据库之间都可能出现数据不一致。常用策略包括 TTL 过期、主动失效、以及组合方案。

4.3.1 TTL 过期策略

为本地缓存和 Redis 分别设置不同的过期时间,利用时间差实现最终一致性。

  • 本地缓存 TTL:通常设置为 5-30 秒,较短,保证数据相对新鲜。
  • Redis TTL:通常设置为 5-60 分钟,较长,作为稳定共享层。
  • 效果:本地缓存过期后自动从 Redis 拉取最新数据,实现最终一致。
// Caffeine 本地缓存配置示例
Cache<String, Object> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.SECONDS)  // 本地缓存 10 秒过期
    .recordStats()
    .build();

// Redis 缓存配置(Spring Data Redis 示例)
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30))  // Redis 缓存 30 分钟过期
        .disableCachingNullValues();
    return RedisCacheManager.builder(factory)
        .cacheDefaults(config)
        .build();
}

4.3.2 主动失效策略

当数据发生写操作(更新/删除)时,主动通知各级缓存失效,避免脏数据。

  • 失效流程:写请求 → 更新数据库 → 删除 Redis 缓存 → 广播失效消息(如 Redis Pub/Sub 或 MQ)→ 各服务节点删除本地缓存。
  • 关键点:先更新数据库,再删除缓存,避免并发读写导致的不一致。
注意: 主动失效策略需要处理消息丢失或重复消费问题。建议结合 TTL 兜底,即使失效消息丢失,本地缓存也会在 TTL 到期后自动刷新。

4.3.3 组合策略:TTL + 主动失效

生产环境推荐组合使用,兼顾实时性与可靠性。

策略 优点 缺点 适用场景
仅 TTL 实现简单,无额外依赖 数据更新延迟大(最长 TTL 时间) 读多写少、对实时性要求不高的场景
仅主动失效 实时性高,数据一致性好 依赖消息中间件,增加系统复杂度 写频繁、对一致性要求高的场景
TTL + 主动失效 实时性高 + 兜底保障 实现复杂度中等 推荐: 大多数企业级业务
组合策略:TTL + 主动失效流程图 写请求到达 更新数据库 删除 Redis 缓存 广播失效消息 节点1 删除 L1 节点2 删除 L1 节点N 删除 L1 兜底机制: 即使失效消息丢失,本地缓存 TTL 到期后自动从 Redis 拉取

4.4 两级缓存代码实现示例

以下展示一个完整的 Caffeine + Redis 两级缓存工具类,包含读穿透、写失效、TTL 兜底。

@Component
public class TwoLevelCache<K, V> {
    private final Cache<K, V> localCache;
    private final RedisTemplate<K, V> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final long redisTtlSeconds;

    public TwoLevelCache(RedisTemplate<K, V> redisTemplate,
                         StringRedisTemplate stringRedisTemplate,
                         long redisTtlSeconds) {
        this.localCache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.SECONDS)  // L1 TTL 10秒
                .recordStats()
                .build();
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        this.redisTtlSeconds = redisTtlSeconds;
    }

    // 读操作:L1 -> L2 -> DB
    public V get(K key, Function<K, V> dbLoader) {
        // 1. 查本地缓存
        V value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        // 2. 查 Redis
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);  // 回填 L1
            return value;
        }
        // 3. 查数据库
        value = dbLoader.apply(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, redisTtlSeconds, TimeUnit.SECONDS);
            localCache.put(key, value);
        }
        return value;
    }

    // 写操作:更新 DB -> 删除 L2 -> 广播失效 L1
    public void put(K key, V value, Consumer<K> dbUpdater) {
        // 1. 更新数据库
        dbUpdater.accept(key);
        // 2. 删除 Redis 缓存
        redisTemplate.delete(key);
        // 3. 广播失效消息(所有节点删除本地缓存)
        stringRedisTemplate.convertAndSend("cache:invalidate", key.toString());
        // 4. 删除当前节点本地缓存
        localCache.invalidate(key);
    }

    // 监听失效消息(在 @RedisListener 中调用)
    @RedisListener(channel = "cache:invalidate")
    public void handleInvalidateMessage(K key) {
        localCache.invalidate(key);
    }
}

4.5 性能调优要点

  • 本地缓存大小:根据 JVM 堆内存设置,通常不超过 100MB,避免 GC 压力。
  • TTL 差异化:热点数据本地 TTL 可适当延长(如 30 秒),冷数据缩短(如 5 秒)。
  • 缓存穿透防护:本地缓存和 Redis 均缓存空值(设置短 TTL),防止恶意 key 穿透。
  • 缓存雪崩防护:本地缓存 TTL 添加随机偏移(±20%),避免同时过期。
  • 监控告警:通过 Caffeine 的 recordStats() 监控本地缓存命中率,低于 80% 时触发告警。
生产案例: 某电商平台商品详情页采用 Caffeine(L1 TTL 15秒)+ Redis(L2 TTL 1小时)两级缓存,本地缓存命中率从 65% 提升至 92%,Redis QPS 下降 70%,接口平均响应时间从 12ms 降至 2ms。

4.6 两级缓存架构总结

层级 缓存组件 TTL 建议 容量建议 一致性策略
L1 本地缓存 Caffeine / Guava Cache 5-30 秒 10-100 MB TTL 过期 + 主动失效
L2 Redis 缓存 Redis Cluster / Sentinel 5-60 分钟 根据业务数据量 TTL 过期 + 主动删除
数据库 MySQL / PostgreSQL — — 作为最终数据源

两级缓存架构通过本地缓存扛住热点流量、Redis 提供共享数据层、数据库作为持久化底座,配合 TTL 与主动失效的组合策略,能够在高并发场景下实现高性能与数据一致性的平衡。实际落地时需根据业务读写比例、数据一致性要求、运维成本等因素灵活调整参数。