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

一、引言

在高并发场景下,缓存是提升系统性能的重要手段。单一缓存方案往往难以兼顾访问速度数据共享 :本地缓存(如 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 中引入以下依赖:

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 缓存配置类

java 复制代码
@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 缓存枚举设计

java 复制代码
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)------ 小说信息:

java 复制代码
@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)------ 小说内容:

java 复制代码
@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 缓存
    }
}

定时清理缓存:

java 复制代码
@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 代码示例

java 复制代码
@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 层抽取为独立类:

java 复制代码
@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 层
    }
}

相关推荐
阳光是sunny7 小时前
LangGraph中的Reducer是什么
前端·人工智能·后端
灯澜忆梦7 小时前
GO_并发编程---定时器
开发语言·后端·golang
阳光是sunny7 小时前
从链到图:LangGraph 入门基础全解析
前端·人工智能·后端
皮皮林5517 小时前
开源实力派,给本地 AI 装上深度调研能力,一张 3090 跑到 95.7 分!
后端
努力的小雨8 小时前
把 Windows 桌面图标做成一个会自己运转的小世界
后端
武子康9 小时前
Search Console Platform Properties 扩大 SEO 资产边界:从 Page Ranking 到 Topic Coverage(5 类误读边界 + 3 表数据层设计)
前端·人工智能·后端
腾渊信息科技公司9 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
IT_陈寒10 小时前
Vue这个特性差点让我加班到凌晨,谁懂啊
前端·人工智能·后端
段一凡-华北理工大学11 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
码事漫谈11 小时前
1999年的电商前夜和2026年的AI前夜 历史押韵但从不重复
后端