1. 项目概述
在Spring Boot项目中,缓存是提升系统性能的重要手段。传统做法是直接注入RedisTemplate或使用Caffeine等本地缓存,但这种做法存在明显的耦合问题:业务代码与具体缓存实现紧密绑定,当需要切换缓存类型或升级缓存架构时,往往需要大面积修改代码。
本文介绍一种基于Spring Cache抽象与条件装配的可插拔缓存方案,具有以下核心优势:
- 一行配置切换:只需修改配置文件中的
lanjii.cache.type值,即可在Caffeine本地缓存与Redis分布式缓存之间无缝切换 - 业务代码零改动:业务层仅依赖Spring Cache标准接口,完全解耦具体实现
- 透明多租户支持:通过装饰器模式自动处理租户隔离,业务开发无需关心Key前缀
- 灵活配置:支持按缓存定义独立设置TTL、最大容量等参数
2. 核心设计思路
2.1 Spring Cache抽象层
Spring Cache是Spring框架提供的缓存抽象层,其核心接口包括:
- CacheManager:缓存管理器,负责创建和管理Cache实例
- Cache:缓存操作接口,提供get、put、evict、clear等方法
关键设计原则:业务代码只依赖CacheManager和Cache接口,不直接引用Caffeine或Redis的具体实现类。这是实现无缝切换的基础。
2.2 条件装配机制
通过Spring Boot的@ConditionalOnProperty注解,根据配置文件中的lanjii.cache.type值决定创建哪种CacheManager:
java复制@Bean
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "LOCAL", matchIfMissing = true)
public CacheManager caffeineCacheManager() {
// 创建Caffeine CacheManager
}
@Bean
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "REDIS")
public CacheManager redisCacheManager() {
// 创建Redis CacheManager
}
matchIfMissing = true表示当配置缺失时默认使用LOCAL模式,确保系统能直接运行。
3. 详细实现步骤
3.1 依赖配置
在pom.xml中同时引入Caffeine和Redis依赖:
xml复制<dependencies>
<!-- Spring Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
虽然两个缓存实现同时存在于classpath,但通过条件装配只会激活其中一个。
3.2 缓存配置属性
定义缓存配置属性类,映射YAML配置:
java复制@Data
@ConfigurationProperties(prefix = "lanjii.cache")
public class LanjiiCacheProperties {
/**
* 缓存类型:LOCAL(Caffeine)或 REDIS
*/
private CacheType type = CacheType.LOCAL;
/**
* 是否允许缓存Null值(防止缓存穿透)
*/
private boolean cacheNullValues = true;
/**
* 默认过期时间,默认1小时
*/
private Duration defaultTtl = Duration.ofHours(1);
/**
* 默认最大容量(仅Local模式有效),默认1000
*/
private long defaultMaxSize = 1000L;
public enum CacheType {
LOCAL, // 本地缓存(Caffeine)
REDIS // 分布式缓存(Redis)
}
}
3.3 缓存元数据定义
每个缓存实例都有自己的配置参数,通过CacheDef类封装:
java复制public class CacheDef {
private final String name; // 缓存名称
private final Duration ttl; // 过期时间
private final long maxSize; // 最大容量(仅Local模式)
private final boolean tenantIsolated; // 是否按租户隔离
// 静态工厂方法
public static CacheDef of(String name, Duration ttl) {
return new CacheDef(name, ttl, 1000L, true);
}
// 其他工厂方法...
}
采用静态工厂方法而非Builder模式,因为缓存定义通常是常量,工厂方法更简洁。
3.4 缓存注册表
CacheRegistry作为缓存定义的中央注册表:
java复制public class CacheRegistry {
private final Map<String, CacheDef> cacheDefMap = new ConcurrentHashMap<>();
public void register(CacheDef cacheDef) {
cacheDefMap.put(cacheDef.getName(), cacheDef);
}
public Optional<CacheDef> get(String name) {
return Optional.ofNullable(cacheDefMap.get(name));
}
// 其他方法...
}
使用ConcurrentHashMap保证线程安全,支持多模块并发注册。
3.5 Redis缓存配置
Redis模式下的关键配置:
java复制@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "REDIS")
public class RedisCacheConfiguration {
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
// 配置JSON序列化
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// 默认缓存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.computePrefixWith(cacheName -> {
// 多租户前缀:tenantId:cacheName::
Long tenantId = TenantContext.getTenantId();
return (tenantId != null ? tenantId.toString() : "0") + ":" + cacheName + "::";
});
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}
关键设计点:
- 使用JSON序列化而非Java默认序列化,便于调试
- 通过
computePrefixWith自动添加租户前缀实现隔离 - 双重条件装配确保只在Redis模式下生效
3.6 本地缓存的多租户隔离
本地缓存模式下,通过装饰器模式实现租户隔离:
java复制public class TenantAwareCaffeineCacheManager extends CaffeineCacheManager {
@Override
public Cache getCache(String name) {
Cache delegate = super.getCache(name);
if (delegate == null) {
delegate = createAndRegisterCache(name);
}
// 需要租户隔离的缓存,包装为TenantAwareCache
CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
if (cacheDef != null && cacheDef.isTenantIsolated()) {
return new TenantAwareCache(delegate);
}
return delegate;
}
static class TenantAwareCache implements Cache {
private final Cache delegate;
private Object createTenantKey(Object key) {
Long tenantId = TenantContext.getTenantId();
return (tenantId != null ? tenantId.toString() : "0") + ":" + key;
}
@Override
public ValueWrapper get(Object key) {
return delegate.get(createTenantKey(key));
}
// 其他方法实现...
}
}
TenantAwareCache包装原始Cache,在每次操作时自动添加租户前缀,业务层完全无感知。
4. 业务模块接入
4.1 声明缓存常量
java复制public interface SystemCacheConstants {
/** 字典数据缓存(全租户共享) */
CacheDef DICT_DATA = CacheDef.of("dictData", Duration.ofHours(24), false);
/** 用户信息缓存(租户隔离) */
CacheDef USER_INFO = CacheDef.of("userInfo", Duration.ofHours(24));
}
4.2 启动时注册缓存
java复制@Configuration
@RequiredArgsConstructor
public class SystemCacheConfig {
private final CacheRegistry cacheRegistry;
@PostConstruct
public void registerCaches() {
cacheRegistry.registerAll(
SystemCacheConstants.DICT_DATA,
SystemCacheConstants.USER_INFO
);
}
}
4.3 在Service中使用
java复制@Service
public class UserServiceImpl implements UserService {
private final CacheManager cacheManager;
public User getById(Long id) {
Cache cache = cacheManager.getCache(SystemCacheConstants.USER_INFO.getName());
User user = cache.get(id, User.class);
if (user == null) {
user = userDao.selectById(id);
cache.put(id, user);
}
return user;
}
}
业务代码仅依赖CacheManager接口,完全解耦具体实现。
5. 架构模式切换
5.1 单机模式(Local)
适合单体应用或对延迟敏感的场景:
yaml复制lanjii:
cache:
type: LOCAL
5.2 分布式模式(Redis)
适合微服务架构或多实例部署:
yaml复制lanjii:
cache:
type: REDIS
6. 注意事项与最佳实践
-
序列化安全:
- Redis模式下使用JSON序列化时,避免开启
activateDefaultTyping - 生产环境建议使用
@JsonTypeInfo注解显式声明类型信息
- Redis模式下使用JSON序列化时,避免开启
-
租户上下文:
- 确保请求入口处设置TenantContext
- 未设置租户时默认使用"0"命名空间
-
缓存清除:
clear()会清除所有租户数据- 按租户清除应逐个evict已知key
-
性能调优:
- 本地缓存合理设置maxSize避免内存溢出
- Redis缓存考虑使用pipeline批量操作
-
监控指标:
- Caffeine缓存可暴露指标到Actuator
- Redis缓存监控内存使用和命中率
这套架构在实际项目中已得到验证,能够满足从开发到生产不同环境的需求,特别适合需要支持多租户的SaaS应用。通过抽象层的设计,业务开发可以更专注于业务逻辑,而无需关心底层缓存实现细节。