如何使用 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 层
    }
}

相关推荐
Java水解2 小时前
微服务架构下Spring Session与Redis分布式会话实战全解析
后端·spring
Json_Lee2 小时前
2026 年了,多 Agent 编码该怎么选?agent-team vs Claude Agent Teams vs Claude Squad vs Met
前端·后端·vibecoding
陈随易2 小时前
刚上市就断货?如此火爆的编程显示器到底有什么魔力
前端·后端·程序员
ray_liang3 小时前
一小时手搓轻量级可代替 Qdrant 的向量数据库
后端·架构
昵称为空C3 小时前
spring-ai mcp-server(ssh工具)
后端·ai编程
前端付豪5 小时前
AI 数学辅导老师项目构想和初始化
前端·后端·python
七牛云行业应用5 小时前
保姆级 OpenClaw 避坑指南:手把手教你看日志修 Bug,顺畅连通各大 AI 模型
人工智能·后端·node.js
程序员爱钓鱼5 小时前
Go并发控制核心:context 包完整技术解析
后端·google·go
树獭叔叔5 小时前
OpenClaw Plugins 与 Hooks 系统:让 AI 助手无限可能
后端·aigc·openai