一、引言
在高并发场景下,缓存是提升系统性能的重要手段。单一缓存方案往往难以兼顾访问速度与数据共享:本地缓存(如 Caffeine)读写延迟极低,但无法跨实例共享;分布式缓存(如 Redis)支持多实例共享,但存在网络 IO 开销。二级缓存将两者结合,形成「本地缓存 + 分布式缓存」的分层架构,在保证数据一致性的前提下,显著提升系统吞吐与响应速度。
本文结合小说项目实践,介绍如何基于 Spring Cache 抽象层,集成 Caffeine 与 Redis 构建二级缓存机制,涵盖原理、实现方式及项目中的具体应用。
二、核心概念
2.1 二级缓存架构
| 层级 | 技术选型 | 特点 | 典型场景 |
|---|---|---|---|
| L1 一级缓存 | Caffeine | 纳秒级延迟、无网络 IO、仅当前实例可见 | 热点数据、静态配置 |
| L2 二级缓存 | Redis | 微秒级延迟、支持跨实例共享、存在网络开销 | 用户数据、需共享的业务数据 |
2.2 Spring Cache 抽象层
Spring Cache 通过 CacheManager 和 Cache 接口统一不同缓存实现,开发者通过 @Cacheable、@CachePut、@CacheEvict 等注解即可使用缓存,无需关心底层实现。
- CacheManager:缓存管理器,负责根据名称提供
Cache实例 - Cache:缓存接口,定义
get、put、evict、clear等操作 - @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 的命中判断与回填,无需自定义 Cache 或 CacheManager。
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 层
}
}