如何使用 Spring Cache 结合 Redis 和 Caffeine 构建二级缓存机制

17 阅读5分钟

一、引言

在高并发场景下,缓存是提升系统性能的重要手段。单一缓存方案往往难以兼顾访问速度数据共享:本地缓存(如 Caffeine)读写延迟极低,但无法跨实例共享;分布式缓存(如 Redis)支持多实例共享,但存在网络 IO 开销。二级缓存将两者结合,形成「本地缓存 + 分布式缓存」的分层架构,在保证数据一致性的前提下,显著提升系统吞吐与响应速度。

本文结合小说项目实践,介绍如何基于 Spring Cache 抽象层,集成 CaffeineRedis 构建二级缓存机制,涵盖原理、实现方式及项目中的具体应用。


二、核心概念

2.1 二级缓存架构

层级技术选型特点典型场景
L1 一级缓存Caffeine纳秒级延迟、无网络 IO、仅当前实例可见热点数据、静态配置
L2 二级缓存Redis微秒级延迟、支持跨实例共享、存在网络开销用户数据、需共享的业务数据

2.2 Spring Cache 抽象层

Spring Cache 通过 CacheManagerCache 接口统一不同缓存实现,开发者通过 @Cacheable@CachePut@CacheEvict 等注解即可使用缓存,无需关心底层实现。

  • CacheManager:缓存管理器,负责根据名称提供 Cache 实例
  • Cache:缓存接口,定义 getputevictclear 等操作
  • @Cacheable: 找到缓存直接返回,没找到查询数据库,存入缓存再返回
  • @CachePut:一定会执行方法,再将返回值写入缓存中
  • CacheEvict:强制删除缓存

引入 spring-boot-starter-cache 后,Spring Boot 会根据依赖自动配置对应的 CacheManager(如 Redis、Caffeine)。若是系统中同时存在Redis和Caffeine的依赖,这时spring cache 不会进行自动配置,需要开发者手动配置对应的cachemanager


三、项目依赖配置

pom.xml 中引入以下依赖:

<!-- 缓存相关 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
  • spring-boot-starter-cache:提供 Spring Cache 抽象及 @EnableCaching 支持
  • spring-boot-starter-data-redis:提供 Redis 连接
  • caffeine:高性能本地缓存库

四、项目中的实现方式:双 CacheManager 策略

本小说项目采用双 CacheManager 策略:同时配置 Caffeine 与 Redis 两个 CacheManager,根据业务特性为不同数据选择本地缓存或分布式缓存,而非严格的 L1→L2 级联查询。这种方式实现简单、职责清晰,适合「按业务划分缓存层级」的场景。

4.1 缓存配置类

@Configuration
public class CacheConfig {

    /**
     * Caffeine 缓存管理器(主缓存,用于本地缓存)
     */
    @Bean
    @Primary
    public CacheManager caffeineCacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length);

        for (var c : CacheConsts.CacheEnum.values()) {
            if (c.isLocal()) {
                Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                    .recordStats()
                    .maximumSize(c.getMaxSize());
                if (c.getTtl() > 0) {
                    caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl()));
                }
                caches.add(new CaffeineCache(c.getName(), caffeine.build()));
            }
        }
        cacheManager.setCaches(caches);
        return cacheManager;
    }

    /**
     * Redis 缓存管理器(用于分布式缓存)
     */
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter
            .nonLockingRedisCacheWriter(connectionFactory);

        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration
            .defaultCacheConfig()
            .disableCachingNullValues()
            .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);

        Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>();
        for (var c : CacheConsts.CacheEnum.values()) {
            if (c.isRemote()) {
                if (c.getTtl() > 0) {
                    cacheMap.put(c.getName(),
                        RedisCacheConfiguration.defaultCacheConfig()
                            .disableCachingNullValues()
                            .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
                            .entryTtl(Duration.ofSeconds(c.getTtl())));
                } else {
                    cacheMap.put(c.getName(),
                        RedisCacheConfiguration.defaultCacheConfig()
                            .disableCachingNullValues()
                            .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
                }
            }
        }

        RedisCacheManager redisCacheManager = new RedisCacheManager(
            redisCacheWriter, defaultCacheConfig, cacheMap);
        redisCacheManager.setTransactionAware(true);
        redisCacheManager.initializeCaches();
        return redisCacheManager;
    }
}

要点:

  • @Primary 标记 Caffeine 为主 CacheManager,未显式指定时使用该管理器
  • 通过 CacheEnum 统一管理缓存名称、TTL、最大容量及本地/远程类型

4.2 缓存枚举设计

public enum CacheEnum {
    // type: 0-仅本地 1-本地+远程 2-仅远程
    HOME_BOOK_CACHE(0, HOME_BOOK_CACHE_NAME, 60 * 60 * 24, 1),
    LATEST_NEWS_CACHE(0, LATEST_NEWS_CACHE_NAME, 60 * 10, 1),
    BOOK_VISIT_RANK_CACHE(2, BOOK_VISIT_RANK_CACHE_NAME, 60 * 60 * 6, 1),
    BOOK_NEWEST_RANK_CACHE(0, BOOK_NEWEST_RANK_CACHE_NAME, 60 * 30, 1),
    BOOK_UPDATE_RANK_CACHE(0, BOOK_UPDATE_RANK_CACHE_NAME, 60, 1),
    HOME_FRIEND_LINK_CACHE(2, HOME_FRIEND_LINK_CACHE_NAME, 0, 1),
    BOOK_CATEGORY_LIST_CACHE(0, BOOK_CATEGORY_LIST_CACHE_NAME, 0, 2),
    BOOK_INFO_CACHE(0, BOOK_INFO_CACHE_NAME, 60 * 60 * 18, 500),
    BOOK_CHAPTER_CACHE(0, BOOK_CHAPTER_CACHE_NAME, 60 * 60 * 6, 5000),
    BOOK_CONTENT_CACHE(2, BOOK_CONTENT_CACHE_NAME, 60 * 60 * 12, 3000),
    USER_INFO_CACHE(2, USER_INFO_CACHE_NAME, 60 * 60 * 24, 10000),
    AUTHOR_INFO_CACHE(2, AUTHOR_INFO_CACHE_NAME, 60 * 60 * 48, 1000);

    public boolean isLocal() { return type <= 1; }
    public boolean isRemote() { return type >= 1; }
}
  • type=0:仅本地缓存(Caffeine),如小说分类、首页推荐、新书榜等
  • type=2:仅远程缓存(Redis),如用户信息、作者信息、小说内容、友情链接等需跨实例共享的数据

4.3 业务层使用示例

本地缓存(Caffeine)—— 小说信息:

@Component
@RequiredArgsConstructor
public class BookInfoCacheManager {

    private final BookInfoMapper bookInfoMapper;
    private final BookChapterMapper bookChapterMapper;

    @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto getBookInfo(Long id) {
        return cachePutBookInfo(id);
    }

    @CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto cachePutBookInfo(Long id) {
        // 从数据库加载并组装数据...
        return BookInfoRespDto.builder()...build();
    }

    @CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public void evictBookInfoCache(Long bookId) {
        // 调用此方法自动清除缓存
    }
}

远程缓存(Redis)—— 小说内容:

@Component
@RequiredArgsConstructor
public class BookContentCacheManager {

    private final BookContentMapper bookContentMapper;

    @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
        value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
    public String getBookContent(Long chapterId) {
        BookContent bookContent = bookContentMapper.selectOne(...);
        return bookContent.getContent();
    }

    @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
        value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
    public void evictBookContentCache(Long chapterId) {
        // 清除 Redis 缓存
    }
}

定时清理缓存:

@Scheduled(cron = "0 0 2 * * ?")
@CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
    value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME)
public void clearVisitRankCache() {
    // 每日 2 点清除点击榜缓存
}

五、L1+L2 级联二级缓存

若需要「先查 Caffeine,再查 Redis,最后查 DB」的级联逻辑,可采用方法委托方式:在 Caffeine 注解方法的业务逻辑中,调用使用 Redis CacheManager 的方法,由 Spring 的 @Cacheable 分别完成 L1、L2 的命中判断与回填,无需自定义 CacheCacheManager

5.1 实现思路

  • L1(Caffeine):外层方法使用 @Cacheable(cacheManager = CAFFEINE_CACHE_MANAGER),命中则直接返回,未命中则执行方法体
  • L2(Redis):方法体内调用另一方法,该方法使用 @Cacheable(cacheManager = REDIS_CACHE_MANAGER),命中则返回,未命中则查库并写入 Redis
  • 回填 L1:L2 方法返回后,外层 @Cacheable 自动将结果写入 Caffeine

5.2 代码示例

@Component
@RequiredArgsConstructor
public class BookInfoCacheManager {

    private final BookInfoMapper bookInfoMapper;
    private final BookChapterMapper bookChapterMapper;

    /**
     * L1:使用 Caffeine,命中直接返回;未命中则委托给 L2
     */
    @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto getBookInfo(Long id) {
        return getBookInfoFromRedis(id);
    }

    /**
     * L2:使用 Redis,命中直接返回;未命中则查库并缓存
     */
    @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto getBookInfoFromRedis(Long id) {
        return loadBookInfoFromDb(id);
    }

    private BookInfoRespDto loadBookInfoFromDb(Long id) {
        BookInfo bookInfo = bookInfoMapper.selectById(id);
        // ... 组装并返回 BookInfoRespDto
        return BookInfoRespDto.builder()...build();
    }

    /**
     * 失效时需同时清除 L1 和 L2
     */
    @CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public void evictBookInfoCache(Long bookId) { }

    @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public void evictBookInfoCacheFromRedis(Long bookId) { }
}

注意getBookInfo(id) 必须通过另一个 Bean 调用 getBookInfoFromRedis(id),否则同一类内自调用会导致 @Cacheable 代理不生效。详细原因可以通过学习文章:juejin.cn/post/757432… 了解。可以将 Redis 层抽取为独立类:

@Component
@RequiredArgsConstructor
public class BookInfoRedisCacheManager {
    private final BookInfoMapper bookInfoMapper;
    private final BookChapterMapper bookChapterMapper;

    @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto getBookInfo(Long id) {
        return loadBookInfoFromDb(id);
    }
    // loadBookInfoFromDb 实现...
}

@Component
@RequiredArgsConstructor
public class BookInfoCacheManager {
    private final BookInfoRedisCacheManager redisCacheManager;

    @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
        value = CacheConsts.BOOK_INFO_CACHE_NAME)
    public BookInfoRespDto getBookInfo(Long id) {
        return redisCacheManager.getBookInfo(id);  // 委托给 Redis 层
    }
}