目录
正在加载目录…
专栏文章
专栏文章
Redis 专栏
1. Redis:核心数据结构与应用场景 2. Redis 数据结构:底层实现与性能取舍 3. Redis 持久化与复制:RDB、AOF 与主从同步 4. Redis 缓存设计:穿透、击穿与雪崩治理 5. Redis 高可用:哨兵、集群与故障转移 6. Redis 多级缓存:一致性、失效与性能优化

Redis 多级缓存:一致性、失效与性能优化

发布于 2026-07-07 10:10 · 最后编辑于 2026-07-31 15:52 · 字数 1,605 👁 162 次阅读

本文讲解多级缓存的完整设计:Caffeine 本地缓存(L1)、Redis 分布式缓存(L2)、Spring Cache 注解抽象,以及二级缓存联动、缓存一致性与更新策略的工程实践。

目录

章节说明
为什么需要多级缓存本地缓存 vs 分布式缓存的定位
L1 本地缓存 CaffeineW-TinyLFU 算法、核心 API、配置
Spring Cache 抽象@Cacheable / @CacheEvict / @CachePut
二级缓存联动设计L1+L2 手动实现与 Layering Cache
缓存更新策略Cache Aside / Write Through / Write Behind
缓存一致性双写不一致根因与解法
最佳实践选型决策、Key 设计、监控

为什么需要多级缓存

单一缓存层的局限:

问题纯本地缓存(单机)纯 Redis(分布式)
网络开销每次请求 ~1ms 网络往返
集群一致性多实例数据不同步天然共享
容量受 JVM 堆限制(通常 < 1GB)可水平扩展
单点故障服务重启缓存丢失Redis 集群保障

多级缓存的访问链路

graph LR
    Req["请求"] --> L1["L1: Caffeine<br/>本地内存,纳秒级"]
    L1 -->|"未命中"| L2["L2: Redis<br/>分布式,毫秒级"]
    L2 -->|"未命中"| DB["数据库 / 数据源"]
    DB -->|"回填"| L2
    L2 -->|"回填"| L1
    style L1 fill:#cfc,stroke:#060
    style L2 fill:#d1ecf1,stroke:#0c5460
    style DB fill:#f8d7da,stroke:#721c24

选型原则:高频读、数据量小、实时性要求不高 → L1(本地);需要集群共享、数据量大 → L2(Redis);两者结合 → 多级缓存。

L1 本地缓存 Caffeine

Caffeine 是基于 W-TinyLFU 淘汰算法的高性能本地缓存,读写吞吐量接近 ConcurrentHashMap

依赖

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <!-- Spring Boot 已管理版本,无需指定 -->
</dependency>

核心构建与 API

Cache<Long, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)                     // 最多缓存 1 万条
    .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后 10 分钟过期
    .expireAfterAccess(5, TimeUnit.MINUTES)  // 最后访问后 5 分钟过期
    .refreshAfterWrite(1, TimeUnit.MINUTES)  // 写入后 1 分钟后台刷新(不阻塞读)
    .recordStats()                           // 开启统计(命中率等)
    .build();

// 获取,不存在返回 null
User user = cache.getIfPresent(1L);

// 获取,不存在时执行加载函数
User user = cache.get(1L, id -> userRepository.findById(id).orElse(null));

// 手动写入 / 删除
cache.put(1L, user);
cache.invalidate(1L);
cache.invalidateAll();

// 统计信息
CacheStats stats = cache.stats();
log.info("命中率: {}, 加载次数: {}", stats.hitRate(), stats.loadCount());

LoadingCache(自动加载)

LoadingCache<Long, User> loadingCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .refreshAfterWrite(1, TimeUnit.MINUTES)  // 配合 CacheLoader 异步刷新
    .build(id -> userRepository.findById(id).orElse(null));  // CacheLoader

// get() 自动触发加载,无需判断 null
User user = loadingCache.get(1L);

// 批量获取
Map<Long, User> users = loadingCache.getAll(List.of(1L, 2L, 3L));

关键参数选择

场景推荐策略
数据更新后需立即失效expireAfterWrite + 主动 invalidate
容忍短暂旧数据,避免缓存击穿refreshAfterWrite(后台刷新,不阻塞读)
热点数据(访问不均匀)maximumSize + W-TinyLFU 自动淘汰冷数据
软引用(内存不足自动释放)softValues()(但会被 GC 突然回收,慎用)

Spring Cache 抽象

Spring Cache 提供注解层,屏蔽底层缓存实现(Caffeine / Redis / Ehcache 均可)。

配置(Spring Boot 3.x)

spring:
  cache:
    type: caffeine   # 或 redis、composite 等
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=10m
@Configuration
@EnableCaching
public class CacheConfig {

    // 细粒度配置(多个不同策略的 Cache)
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats());
        return manager;
    }
}

核心注解

@Service
public class UserService {

    // 读缓存:先查缓存,未命中执行方法并写入
    @Cacheable(cacheNames = "users", key = "#id",
               condition = "#id > 0",            // 满足条件才缓存
               unless = "#result == null")        // 结果为 null 时不缓存
    public UserDTO getUser(Long id) {
        return userRepository.findById(id).map(UserConverter::toDTO).orElse(null);
    }

    // 更新缓存:方法执行后更新缓存(不影响方法执行)
    @CachePut(cacheNames = "users", key = "#result.id")
    public UserDTO updateUser(UpdateUserRequest req) {
        User user = userRepository.save(UserConverter.fromRequest(req));
        return UserConverter.toDTO(user);
    }

    // 删除缓存:方法执行后删除指定 key
    @CacheEvict(cacheNames = "users", key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    // 删除整个 Cache(allEntries = true)
    @CacheEvict(cacheNames = "users", allEntries = true)
    public void refreshAllUsers() { }

    // 组合多个缓存操作
    @Caching(
        put  = @CachePut(cacheNames = "users",        key = "#result.id"),
        evict = @CacheEvict(cacheNames = "user-list", allEntries = true)
    )
    public UserDTO createUser(CreateUserRequest req) {
        User user = userRepository.save(UserConverter.fromRequest(req));
        return UserConverter.toDTO(user);
    }
}

二级缓存联动设计

Spring Cache 的 @Cacheable 只支持单一缓存层。手动实现 L1+L2 联动更可控:

手动实现

@Service
@RequiredArgsConstructor
public class ProductCacheService {

    private final LoadingCache<Long, Product> localCache;
    private final RedisTemplate<String, Product> redisTemplate;
    private final ProductRepository productRepository;

    private static final Duration L2_TTL = Duration.ofMinutes(30);
    private static final String KEY_PREFIX = "product:";

    public Product getProduct(Long id) {
        // 1. 查 L1(本地 Caffeine)
        Product p = localCache.getIfPresent(id);
        if (p != null) return p;

        // 2. 查 L2(Redis)
        String redisKey = KEY_PREFIX + id;
        p = redisTemplate.opsForValue().get(redisKey);
        if (p != null) {
            localCache.put(id, p);     // 回填 L1
            return p;
        }

        // 3. 查数据库
        p = productRepository.findById(id).orElse(null);
        if (p != null) {
            redisTemplate.opsForValue().set(redisKey, p, L2_TTL); // 回填 L2
            localCache.put(id, p);                                  // 回填 L1
        }
        return p;
    }

    public void evict(Long id) {
        localCache.invalidate(id);                          // 清 L1
        redisTemplate.delete(KEY_PREFIX + id);              // 清 L2
    }
}

集群 L1 同步问题

多个服务实例各自有独立 Caffeine,更新数据后其他实例的 L1 缓存仍是旧值。解决方式:

graph LR
    App1["实例 1<br/>更新数据"] -->|"发布失效消息"| Redis["Redis Pub/Sub<br/>channel: cache.evict"]
    Redis -->|"通知"| App1
    Redis -->|"通知"| App2["实例 2<br/>收到消息→清 L1"]
    Redis -->|"通知"| App3["实例 3<br/>收到消息→清 L1"]
// 发布失效消息
redisTemplate.convertAndSend("cache.evict", "product:" + id);

// 监听失效消息,清本地缓存
@Component
public class CacheEvictListener implements MessageListener {

    private final LoadingCache<Long, Product> localCache;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String key = new String(message.getBody());
        if (key.startsWith("product:")) {
            Long id = Long.parseLong(key.substring("product:".length()));
            localCache.invalidate(id);
        }
    }
}

缓存更新策略

Cache Aside(旁路缓存,最常用)

读:先读缓存 → 未命中 → 读 DB → 写入缓存
写:先更新 DB → 再删除缓存(不是更新缓存!)

删而不是更新的原因:更新操作(先写 DB 再写缓存)在并发下可能用旧值覆盖新值;删除是幂等操作,下次读时自然回填。

Write Through(同步直写)

写:同时写 DB 和缓存(事务保证一致)
读:先读缓存,未命中读 DB 并回填

适合强一致性要求,但写性能下降。

Write Behind(异步回写)

写:先写缓存,异步批量写 DB
读:从缓存读,始终最新

写性能最高,但缓存宕机可能丢失数据。适合日志、访问计数等允许少量丢失的场景。

缓存一致性

双写不一致根因

Cache Aside 删除缓存后、下次读回填前,存在短暂不一致窗口。极端并发下还存在:

线程 A:更新 DB(新值)→ 删除缓存
线程 B:读缓存未命中 → 读 DB(旧值,A 还未提交)→ 写入缓存(旧值)
线程 A:DB 提交完成
→ 结果:缓存存的是旧值,DB 是新值

解法:延迟双删

// 更新 DB
userRepository.save(user);
// 第一次删除缓存
cache.delete(key);
// 延迟后再删一次(等待上面窗口期内的回填完成)
scheduler.schedule(() -> cache.delete(key), 500, TimeUnit.MILLISECONDS);

延迟时间:略大于"读 DB + 写缓存"的时间,通常 200~500ms。

解法:基于 Canal 的 CDC 同步

MySQL binlog → Canal → MQ → 缓存更新服务 → 删除/更新 Redis

业务代码只写 DB,缓存失效由异步 CDC 驱动,彻底解耦。适合一致性要求较高的场景。

最佳实践

Key 设计规范

格式:{业务}:{实体}:{id}[:字段]
示例:
  user:profile:1001
  product:detail:sku:20240101
  order:status:2024010100001
  user:list:status:ACTIVE:page:1

避免:Key 过长(增加内存和网络开销)、Key 随机(难以运维)、Key 包含特殊字符。

缓存监控

// Caffeine 统计
@Scheduled(fixedDelay = 60_000)
public void reportCacheStats() {
    CacheStats stats = cache.stats();
    metrics.gauge("cache.hit.rate", stats.hitRate());
    metrics.counter("cache.miss.count", stats.missCount());
    metrics.timer("cache.load.avg", stats.averageLoadPenalty(), TimeUnit.NANOSECONDS);
}

// Redis 命中率
// INFO stats 命令中的 keyspace_hits / keyspace_misses

选型速查

需求推荐方案
单机,读热点数据Caffeine(L1)
集群,共享数据Redis(L2)
高频读 + 集群部署Caffeine(L1) + Redis(L2) 多级缓存
需要注解简化代码Spring Cache + CaffeineCacheManager
集群 L1 一致性Redis Pub/Sub 通知 + Caffeine invalidate
强一致性(不能有脏数据)不用本地缓存,仅 Redis + 延迟双删

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论