在 Spring Boot 项目中,缓存是很常见的能力。
最直接的写法是业务代码中注入 RedisTemplate,然后手动操作缓存。
例如:
redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().set(key, value);
redisTemplate.delete(key);
这种方式简单直接,但也有明显问题:
业务代码和 Redis 强绑定;
后续想切换本地缓存时,需要改大量代码;
不同模块各自拼缓存 Key,容易不统一;
多租户场景下容易出现缓存串数据;
缓存过期时间、容量、隔离策略缺少统一管理。
如果项目初期是单体部署,可能更适合使用本地缓存。
如果后续变成多实例部署,又可能需要切换成 Redis 分布式缓存。
因此,一个更好的方向是:
业务代码只依赖 Spring Cache 抽象,不直接依赖 Caffeine 或 Redis 的具体实现。
这样可以通过配置在本地缓存和分布式缓存之间切换,业务代码基本不需要改动。
一、设计目标
这套缓存设计主要解决几个问题。
1. 业务代码与缓存实现解耦
业务层不直接使用:
RedisTemplate
也不直接使用:
Caffeine
而是统一依赖 Spring Cache 提供的接口:
CacheManager
Cache
2. 支持一行配置切换缓存类型
通过配置控制使用本地缓存还是 Redis 缓存:
lanjii:
cache:
type: LOCAL
或者:
lanjii:
cache:
type: REDIS
3. 支持缓存元数据统一管理
每个缓存可以单独配置:
缓存名称;
过期时间;
最大容量;
是否启用租户隔离。
4. 支持多租户缓存隔离
在多租户系统中,同一个缓存 Key 在不同租户下不能互相污染。
例如:
租户 A:userInfo::admin
租户 B:userInfo::admin
如果不加隔离,就可能出现租户 A 读到租户 B 缓存数据的问题。
因此需要在缓存底层统一加租户前缀,而不是让业务代码手动拼接。
二、Spring Cache 抽象
Spring Cache 提供了一套缓存抽象。
核心接口主要有两个。
1. CacheManager
CacheManager 负责创建和管理缓存实例。
常见方法:
Cache getCache(String name);业务代码可以通过缓存名称获取对应的 Cache。
2. Cache
Cache 负责具体缓存操作。
常见方法包括:
get()
put()
evict()
clear()业务代码只要依赖这两个接口,就可以做到不关心底层缓存实现。
底层可以是:
Caffeine;
Redis;
其他缓存实现。
这也是后续实现缓存切换的基础。
三、整体架构思路
整体架构可以拆成几层:
业务代码
↓
Spring Cache 接口
↓
CacheManager
↓
条件装配选择具体实现
↓
Caffeine / Redis
缓存类型由配置决定:
lanjii:
cache:
type: LOCAL
可选值:
LOCAL:使用 Caffeine 本地缓存
REDIS:使用 Redis 分布式缓存核心实现方式是:
使用
@ConfigurationProperties读取缓存配置;使用
@ConditionalOnProperty根据配置创建不同的CacheManager;使用
CacheDef定义每个缓存的元数据;使用
CacheRegistry统一注册缓存定义;使用装饰器模式处理本地缓存的租户隔离;
Redis 通过 Key 前缀处理租户隔离。
四、引入依赖
缓存模块中需要引入 Spring Cache、Caffeine 和 Redis 相关依赖。
<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>
<!-- JSON 序列化依赖 -->
<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>
<!-- Redis 分布式缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
这里同时引入 Caffeine 和 Redis 依赖。
真正运行时使用哪个缓存,由条件装配决定。
五、定义缓存配置属性
先定义缓存配置属性类,用来映射配置文件中的参数。
package com.lanjii.framework.cache.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
@Data
@ConfigurationProperties(prefix = "lanjii.cache")
public class LanjiiCacheProperties {
/**
* 缓存类型:LOCAL / REDIS
*/
private CacheType type = CacheType.LOCAL;
/**
* 是否缓存空值
*/
private boolean cacheNullValues = true;
/**
* 默认过期时间
*/
private Duration defaultTtl = Duration.ofHours(1);
/**
* Caffeine 默认最大条目数
*/
private long defaultMaxSize = 1000L;
public enum CacheType {
LOCAL,
REDIS
}
}
配置示例:
lanjii:
cache:
type: LOCAL
cache-null-values: true
default-ttl: 1h
default-max-size: 1000
字段说明:
六、定义缓存元数据 CacheDef
每个缓存通常会有自己的配置。
例如:
用户信息缓存 24 小时;
验证码缓存 5 分钟;
字典缓存 1 天;
系统配置缓存 7 天;
某些缓存需要租户隔离,某些缓存不需要。
可以用 CacheDef 统一描述这些信息。
package com.lanjii.framework.cache.core;
import java.time.Duration;
public class CacheDef {
private final String name;
private final Duration ttl;
private final long maxSize;
private final boolean tenantIsolated;
private CacheDef(String name, Duration ttl, long maxSize, boolean tenantIsolated) {
this.name = name;
this.ttl = ttl;
this.maxSize = maxSize;
this.tenantIsolated = tenantIsolated;
}
public static CacheDef of(String name, Duration ttl) {
return new CacheDef(name, ttl, 1000L, true);
}
public static CacheDef of(String name, Duration ttl, long maxSize) {
return new CacheDef(name, ttl, maxSize, true);
}
public static CacheDef of(String name, Duration ttl, boolean tenantIsolated) {
return new CacheDef(name, ttl, 1000L, tenantIsolated);
}
public static CacheDef of(String name, Duration ttl, long maxSize, boolean tenantIsolated) {
return new CacheDef(name, ttl, maxSize, tenantIsolated);
}
public String getName() {
return name;
}
public Duration getTtl() {
return ttl;
}
public long getMaxSize() {
return maxSize;
}
public boolean isTenantIsolated() {
return tenantIsolated;
}
}
这里用静态工厂方法创建缓存定义。
例如:
CacheDef USER_INFO = CacheDef.of("userInfo", Duration.ofHours(24));表示:
缓存名:
userInfo;过期时间:24 小时;
默认开启租户隔离。
如果是全局共享缓存,可以关闭租户隔离:
CacheDef DICT_DATA = CacheDef.of("dictData", Duration.ofHours(24), false);七、定义缓存注册表 CacheRegistry
缓存定义需要统一注册和管理。
package com.lanjii.framework.cache.core;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class CacheRegistry {
private final ConcurrentMap<String, CacheDef> cacheDefMap = new ConcurrentHashMap<>();
public void register(CacheDef cacheDef) {
cacheDefMap.put(cacheDef.getName(), cacheDef);
}
public void registerAll(CacheDef... cacheDefs) {
for (CacheDef cacheDef : cacheDefs) {
register(cacheDef);
}
}
public Optional<CacheDef> get(String name) {
return Optional.ofNullable(cacheDefMap.get(name));
}
public Collection<CacheDef> getAll() {
return cacheDefMap.values();
}
}
这里使用 ConcurrentHashMap,是为了避免多个模块启动时并发注册缓存定义带来的线程安全问题。
八、自动配置入口
定义自动配置类,根据配置选择本地缓存或 Redis 缓存。
package com.lanjii.framework.cache.config;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@EnableCaching
@EnableConfigurationProperties(LanjiiCacheProperties.class)
public class LanjiiCacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CacheRegistry cacheRegistry() {
return new CacheRegistry();
}
@Bean
@ConditionalOnProperty(
name = "lanjii.cache.type",
havingValue = "LOCAL",
matchIfMissing = true
)
public CacheManager caffeineCacheManager(CacheRegistry cacheRegistry,
LanjiiCacheProperties properties) {
return new TenantAwareCaffeineCacheManager(cacheRegistry, properties);
}
}
这里的关键点是:
@ConditionalOnProperty(
name = "lanjii.cache.type",
havingValue = "LOCAL",
matchIfMissing = true
)
表示:
当
lanjii.cache.type=LOCAL时,创建 Caffeine 缓存管理器;如果没有配置该字段,也默认使用本地缓存。
九、Redis CacheManager 配置
当配置为:
lanjii:
cache:
type: REDIS时,创建 Redis 版本的 CacheManager。
package com.lanjii.framework.cache.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import com.lanjii.framework.context.tenant.TenantContext;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "REDIS")
public class RedisCacheConfigurationConfig {
@Bean
@ConditionalOnClass(RedisConnectionFactory.class)
public CacheManager redisCacheManager(CacheRegistry cacheRegistry,
LanjiiCacheProperties properties,
RedisConnectionFactory connectionFactory) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(properties.getDefaultTtl())
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(serializer)
)
.computePrefixWith(cacheName -> {
Long tenantId = TenantContext.getTenantId();
String prefix = tenantId != null ? tenantId.toString() : "0";
return prefix + ":" + cacheName + "::";
});
if (!properties.isCacheNullValues()) {
defaultConfig = defaultConfig.disableCachingNullValues();
}
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
for (CacheDef cacheDef : cacheRegistry.getAll()) {
configMap.put(
cacheDef.getName(),
defaultConfig.entryTtl(cacheDef.getTtl())
);
}
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.build();
}
}
这里主要做了几件事:
配置 JSON 序列化;
支持 Java 8 时间类型;
配置默认过期时间;
根据
CacheDef配置每个缓存的独立过期时间;通过
computePrefixWith给缓存 Key 添加租户前缀。
Redis Key 格式类似:
{tenantId}:{cacheName}::{key}例如:
1001:userInfo::admin这样可以按租户和缓存名进行隔离,也方便在 Redis 可视化工具中排查问题。
十、关于 Redis 序列化的注意点
示例中使用了:
GenericJackson2JsonRedisSerializer
相比 JDK 默认序列化,JSON 更适合排查问题。
但需要注意这段配置:
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);它会在 JSON 中写入类型信息,方便反序列化时还原对象类型。
但这种方式也有安全风险。
如果 Redis 暴露在不可信环境中,或者反序列化数据来源不可控,就不建议直接使用这种方式。
更稳妥的做法是:
Redis 不暴露到公网;
使用强密码和访问控制;
对缓存对象使用明确 DTO;
必要时自定义序列化器;
或者显式使用
@JsonTypeInfo控制类型信息。
十一、本地缓存多租户隔离
Redis 可以通过 Key 前缀实现多租户隔离。
但 Caffeine 是本地缓存,如果直接使用同一个缓存名和同一个 Key,就可能出现不同租户之间的数据串读。
例如:
租户 1001:userInfo::admin
租户 1002:userInfo::admin如果不做处理,两个租户都会使用同一个 admin 作为缓存 Key。
解决方式是使用装饰器模式包装 Cache,在底层自动给 Key 添加租户前缀。
十二、支持租户隔离的 CaffeineCacheManager
package com.lanjii.framework.cache.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import com.lanjii.framework.context.tenant.TenantContext;
import org.springframework.cache.Cache;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import java.util.concurrent.Callable;
public class TenantAwareCaffeineCacheManager extends CaffeineCacheManager {
private final CacheRegistry cacheRegistry;
private final LanjiiCacheProperties properties;
public TenantAwareCaffeineCacheManager(CacheRegistry cacheRegistry,
LanjiiCacheProperties properties) {
this.cacheRegistry = cacheRegistry;
this.properties = properties;
this.setAllowNullValues(properties.isCacheNullValues());
}
@Override
public Cache getCache(String name) {
Cache delegate = super.getCache(name);
if (delegate == null) {
delegate = createAndRegisterCache(name);
}
if (delegate == null) {
return null;
}
CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
if (cacheDef != null && cacheDef.isTenantIsolated()) {
return new TenantAwareCache(delegate);
}
return delegate;
}
private synchronized Cache createAndRegisterCache(String name) {
Cache existing = super.getCache(name);
if (existing != null) {
return existing;
}
CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
Caffeine<Object, Object> builder = Caffeine.newBuilder();
if (cacheDef != null) {
builder.expireAfterWrite(cacheDef.getTtl())
.maximumSize(cacheDef.getMaxSize());
} else {
builder.expireAfterWrite(properties.getDefaultTtl())
.maximumSize(properties.getDefaultMaxSize());
}
this.registerCustomCache(name, builder.build());
return super.getCache(name);
}
static class TenantAwareCache implements Cache {
private final Cache delegate;
public TenantAwareCache(Cache delegate) {
this.delegate = delegate;
}
private Object createTenantKey(Object key) {
Long tenantId = TenantContext.getTenantId();
String prefix = tenantId != null ? tenantId.toString() : "0";
return prefix + ":" + key;
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public Object getNativeCache() {
return delegate.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
return delegate.get(createTenantKey(key));
}
@Override
public <T> T get(Object key, Class<T> type) {
return delegate.get(createTenantKey(key), type);
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return delegate.get(createTenantKey(key), valueLoader);
}
@Override
public void put(Object key, Object value) {
delegate.put(createTenantKey(key), value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
return delegate.putIfAbsent(createTenantKey(key), value);
}
@Override
public void evict(Object key) {
delegate.evict(createTenantKey(key));
}
@Override
public boolean evictIfPresent(Object key) {
return delegate.evictIfPresent(createTenantKey(key));
}
@Override
public void clear() {
delegate.clear();
}
@Override
public boolean invalidate() {
return delegate.invalidate();
}
}
}这里的关键逻辑是:
private Object createTenantKey(Object key) {
Long tenantId = TenantContext.getTenantId();
String prefix = tenantId != null ? tenantId.toString() : "0";
return prefix + ":" + key;
}所有缓存操作都会先把原始 Key 转成带租户前缀的 Key。
例如:
原始 Key:admin
租户 ID:1001
最终 Key:1001:admin这样业务层不需要关心租户前缀问题。
十三、多租户缓存使用注意点
1. 请求入口必须设置租户上下文
缓存隔离依赖:
TenantContext.getTenantId()所以必须在请求入口设置租户上下文。
例如:
Filter;
Interceptor;
Gateway 透传;
登录上下文解析。
如果租户上下文没有正确设置,就会使用默认前缀:
0:这可能导致不同请求的数据混在一起。
2. clear() 要谨慎使用
当前装饰器中的:
clear()会清空整个缓存名下的数据。
也就是说,如果一个缓存名下有多个租户的数据,调用 clear() 会清掉所有租户的数据。
如果只想删除当前租户的数据,需要按已知 Key 调用:
evict()或者单独设计按租户清理缓存的能力。
3. 全局共享缓存可以关闭租户隔离
例如:
字典数据;
系统配置;
公共枚举;
全局开关。
这些数据如果所有租户共享,可以关闭租户隔离:
CacheDef DICT_DATA = CacheDef.of("dictData", Duration.ofHours(24), false);这样可以减少重复缓存,降低内存占用。
十四、缓存辅助工具 CacheHelper
Spring Cache 提供的是通用缓存接口。
如果业务中需要一些扩展操作,可以封装一个辅助工具类。
例如获取本地缓存中的所有值。
package com.lanjii.framework.cache.helper;
import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.context.tenant.TenantContext;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class CacheHelper {
private final CacheManager cacheManager;
private final CacheRegistry cacheRegistry;
public <T> Collection<T> getAllValues(String cacheName, Class<T> type) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
return Collections.emptyList();
}
Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof com.github.benmanes.caffeine.cache.Cache) {
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache =
(com.github.benmanes.caffeine.cache.Cache<Object, Object>) nativeCache;
CacheDef cacheDef = cacheRegistry.get(cacheName).orElse(null);
boolean tenantIsolated = cacheDef == null || cacheDef.isTenantIsolated();
if (tenantIsolated) {
Long tenantId = TenantContext.getTenantId();
String tenantPrefix = (tenantId != null ? tenantId.toString() : "0") + ":";
return caffeineCache.asMap().entrySet().stream()
.filter(entry -> entry.getKey().toString().startsWith(tenantPrefix))
.map(entry -> type.cast(entry.getValue()))
.collect(Collectors.toList());
}
return caffeineCache.asMap().values().stream()
.map(type::cast)
.collect(Collectors.toList());
}
throw new UnsupportedOperationException("getAllValues is only supported in LOCAL mode");
}
public void clearAll(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
public Cache getCache(String cacheName) {
return cacheManager.getCache(cacheName);
}
}
这里要注意:
getAllValues()
只适合本地缓存模式。
如果是 Redis 模式,不建议使用 KEYS * 遍历缓存 Key,因为可能阻塞 Redis。
需要遍历时,应使用 SCAN 之类的方式,并谨慎控制扫描范围。
十五、自动配置注册
如果这是一个独立的缓存 starter 模块,需要让 Spring Boot 自动识别配置类。
Spring Boot 3.x 可以在下面文件中注册:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容:
com.lanjii.framework.cache.config.LanjiiCacheAutoConfiguration这样业务项目引入缓存模块后,就可以自动加载缓存配置。
十六、业务模块如何接入
业务模块接入时,通常分三步。
1. 定义缓存常量
package com.lanjii.sys.config;
import com.lanjii.framework.cache.core.CacheDef;
import java.time.Duration;
public interface SystemCacheConstants {
/**
* 字典数据缓存,全局共享
*/
CacheDef DICT_DATA = CacheDef.of("dictData", Duration.ofHours(24), false);
/**
* 系统配置缓存,全局共享
*/
CacheDef SYS_CONFIG = CacheDef.of("sysConfig", Duration.ofDays(7), false);
/**
* 用户会话缓存,全局共享
*/
CacheDef USER_SESSION = CacheDef.of("userSession", Duration.ofHours(24), false);
/**
* 用户信息缓存,租户隔离
*/
CacheDef USER_INFO = CacheDef.of("userInfo", Duration.ofHours(24));
/**
* 验证码缓存,租户隔离
*/
CacheDef CAPTCHA = CacheDef.of("captcha", Duration.ofMinutes(5));
}
2. 启动时注册缓存定义
package com.lanjii.sys.config;
import com.lanjii.framework.cache.core.CacheRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class SystemCacheConfig {
private final CacheRegistry cacheRegistry;
@PostConstruct
public void registerCaches() {
cacheRegistry.registerAll(
SystemCacheConstants.DICT_DATA,
SystemCacheConstants.SYS_CONFIG,
SystemCacheConstants.USER_SESSION,
SystemCacheConstants.USER_INFO,
SystemCacheConstants.CAPTCHA
);
}
}
3. 业务层使用缓存
业务层只依赖 CacheManager 和 Cache。
package com.lanjii.sys.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lanjii.framework.cache.helper.CacheHelper;
import com.lanjii.sys.config.SystemCacheConstants;
import com.lanjii.sys.dto.SysConfigDTO;
import com.lanjii.sys.entity.SysConfig;
import com.lanjii.sys.service.SysConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
@Service("sysConfigService")
@RequiredArgsConstructor
public class SysConfigServiceImpl implements SysConfigService {
private final CacheManager cacheManager;
private final CacheHelper cacheHelper;
private Cache getConfigCache() {
return cacheManager.getCache(SystemCacheConstants.SYS_CONFIG.getName());
}
@Override
public SysConfig getConfigByKey(String configKey) {
Cache cache = getConfigCache();
SysConfig config = cache.get(configKey, SysConfig.class);
if (config != null) {
return config;
}
config = getOne(new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getConfigKey, configKey));
if (config != null) {
cache.put(configKey, config);
}
return config;
}
@Override
public void updateByIdNew(Long id, SysConfigDTO dto) {
SysConfig originalConfig = getById(id);
SysConfig entity = convertToEntity(dto);
updateById(entity);
getConfigCache().evict(originalConfig.getConfigKey());
}
@Override
public void clearCache() {
cacheHelper.clearAll(SystemCacheConstants.SYS_CONFIG.getName());
}
private SysConfig convertToEntity(SysConfigDTO dto) {
return new SysConfig();
}
}
这里业务代码没有直接依赖 Caffeine 或 Redis。
所以后续切换缓存类型时,不需要修改业务代码。
十七、缓存模式切换
1. 本地缓存模式
适合单体应用、小型私有化部署、低延迟场景。
lanjii:
cache:
type: LOCAL
default-ttl: 1h
default-max-size: 2000
如果不配置 type,默认也是 LOCAL。
2. Redis 缓存模式
适合多实例部署、微服务架构、需要共享缓存的场景。
lanjii:
cache:
type: REDIS
default-ttl: 2h
cache-null-values: true
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
如果项目是集群部署,使用本地缓存会出现不同节点缓存不一致的问题。
这时更适合切换到 Redis。
十八、这种方案的优点
1. 业务代码解耦
业务代码只依赖:
CacheManager
Cache不依赖底层缓存实现。
2. 缓存类型可切换
通过配置即可切换:
lanjii:
cache:
type: LOCAL或者:
lanjii:
cache:
type: REDIS3. 统一管理缓存定义
所有缓存名称、过期时间、最大容量、租户隔离策略都通过 CacheDef 管理。
避免每个业务模块各自乱写缓存 Key 和过期时间。
4. 支持多租户隔离
Redis 通过 Key 前缀隔离。
Caffeine 通过装饰器包装 Cache 实现 Key 前缀隔离。
业务层不需要手动拼接租户 ID。
5. 适配不同部署场景
单机部署可以使用 Caffeine。
集群部署可以切换 Redis。
同一套业务代码可以适配不同环境。
十九、需要注意的问题
1. 本地缓存不适合集群共享数据
Caffeine 是进程内缓存。
如果应用部署多个实例,每个实例都有自己的缓存副本。
如果业务需要多节点实时共享缓存,应使用 Redis。
2. 缓存清理要考虑租户范围
当前 clear() 是清空整个缓存。
如果缓存启用了租户隔离,直接调用 clear() 可能会清空所有租户的数据。
需要谨慎使用。
3. 租户上下文必须可靠
多租户隔离依赖 TenantContext。
如果租户上下文没有正确初始化,缓存 Key 会使用默认租户前缀,可能导致隔离失效。
4. Redis 序列化要注意安全
如果使用 JSON 类型信息反序列化,需要确保 Redis 数据来源可信。
不要把 Redis 暴露到不可信网络环境。
5. 缓存空值要结合业务判断
缓存空值可以缓解缓存穿透。
但如果空值缓存时间太长,可能导致后续真实数据创建后仍然读到空值。
因此空值缓存要结合业务设置合理 TTL。
结论
这套缓存架构的核心思路是:
业务代码只依赖 Spring Cache 抽象,通过条件装配在 Caffeine 和 Redis 之间切换。
整体实现包括:
使用
CacheManager和Cache解耦业务代码;使用
LanjiiCacheProperties管理缓存配置;使用
CacheDef定义缓存元数据;使用
CacheRegistry注册所有缓存定义;使用
@ConditionalOnProperty实现缓存类型切换;Redis 通过 Key 前缀实现多租户隔离;
Caffeine 通过装饰器模式实现多租户隔离;
业务模块只需要注册缓存定义并使用 Spring Cache 接口。
如果项目需要同时适配单机部署和集群部署,可以基于 Spring Cache 抽象封装一层缓存架构,让业务代码不直接依赖 Caffeine 或 Redis,再通过配置决定最终使用哪种缓存实现。