在当今的企业级应用开发中,缓存技术已经成为提升系统性能不可或缺的一环。作为一名长期奋战在一线的Java开发者,我深刻体会到直接使用RedisTemplate这类具体实现带来的耦合问题。当项目需要从本地缓存切换到分布式缓存,或者反过来时,往往意味着大量的代码重构。今天我要分享的这套方案,正是为了解决这个痛点而生。
这套架构的核心价值在于:
Spring Cache作为框架提供的缓存抽象,其核心接口非常简单:
java复制public interface CacheManager {
Cache getCache(String name);
}
public interface Cache {
ValueWrapper get(Object key);
void put(Object key, Object value);
void evict(Object key);
void clear();
}
这种设计的美妙之处在于:业务代码只需要与这两个接口打交道,完全不需要关心底层是使用内存缓存还是分布式缓存。这为我们实现可插拔的缓存架构奠定了理论基础。
我们的架构设计遵循以下原则:
架构示意图如下:
code复制[业务代码] --> [Spring Cache抽象层]
↑
[CacheManager工厂]
↑
+-----------+-----------+
| |
[Caffeine实现] [Redis实现]
多租户场景下的缓存隔离是个容易被忽视的问题。我们的解决方案是:
这种设计确保了:
首先需要在pom.xml中配置必要的依赖:
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>
<!-- JSON序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
关键点:
定义配置属性类,用于接收application.yml中的配置:
java复制@Data
@ConfigurationProperties(prefix = "app.cache")
public class CacheProperties {
private CacheType type = CacheType.LOCAL;
private boolean cacheNullValues = true;
private Duration defaultTtl = Duration.ofHours(1);
private long defaultMaxSize = 1000L;
public enum CacheType {
LOCAL, REDIS
}
}
配置示例:
yaml复制app:
cache:
type: REDIS
default-ttl: 30m
cache-null-values: false
每个缓存实例都有自己的配置,我们通过CacheDef类来封装这些元数据:
java复制public class CacheDef {
private final String name;
private final Duration ttl;
private final long maxSize;
private final boolean tenantIsolated;
// 静态工厂方法
public static CacheDef of(String name, Duration ttl) {
return new CacheDef(name, ttl, 1000L, true);
}
// 其他工厂方法...
}
使用示例:
java复制public interface UserCacheConstants {
CacheDef USER_INFO = CacheDef.of("userInfo", Duration.ofHours(1));
}
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));
}
}
这是实现一键切换的关键:
java复制@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.cache.type", havingValue = "LOCAL", matchIfMissing = true)
public CacheManager caffeineCacheManager(CacheRegistry registry, CacheProperties properties) {
return new TenantAwareCaffeineCacheManager(registry, properties);
}
@Bean
@ConditionalOnProperty(name = "app.cache.type", havingValue = "REDIS")
public CacheManager redisCacheManager(CacheRegistry registry,
CacheProperties properties,
RedisConnectionFactory factory) {
// Redis配置实现...
}
}
对于Caffeine实现的多租户支持:
java复制public class TenantAwareCaffeineCacheManager extends CaffeineCacheManager {
@Override
public Cache getCache(String name) {
Cache cache = super.getCache(name);
CacheDef def = cacheRegistry.get(name).orElse(null);
if (def != null && def.isTenantIsolated()) {
return new TenantAwareCache(cache);
}
return cache;
}
private static class TenantAwareCache implements Cache {
private final Cache delegate;
public Object get(Object key) {
String tenantKey = getTenantPrefix() + key;
return delegate.get(tenantKey);
}
// 其他方法实现...
}
}
java复制@Bean
public RedisCacheManager redisCacheManager(/*...*/) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(mapper);
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.computePrefixWith(this::getCacheKeyPrefix);
// 更多配置...
}
我们采用以下key格式:
code复制{tenantId}:{cacheName}::{actualKey}
例如:
code复制1001:userInfo::admin
这种设计的好处:
重要安全提示:使用Jackson的activateDefaultTyping虽然方便,但在生产环境中可能存在安全风险。建议:
- 对于内部系统,可以开启以获得更好的开发体验
- 对于公网系统,应该禁用此功能,改用更安全的序列化方案
java复制public interface SystemCacheConstants {
// 全局共享缓存
CacheDef APP_CONFIG = CacheDef.of("appConfig", Duration.ofDays(1), false);
// 租户隔离缓存
CacheDef USER_PROFILE = CacheDef.of("userProfile", Duration.ofHours(2));
}
java复制@Configuration
@RequiredArgsConstructor
public class CacheConfig {
private final CacheRegistry registry;
@PostConstruct
public void registerCaches() {
registry.registerAll(
SystemCacheConstants.APP_CONFIG,
SystemCacheConstants.USER_PROFILE
);
}
}
java复制@Service
@RequiredArgsConstructor
public class UserService {
private final CacheManager cacheManager;
public User getUser(String userId) {
Cache cache = cacheManager.getCache("userProfile");
User user = cache.get(userId, User.class);
if (user == null) {
user = loadFromDatabase(userId);
cache.put(userId, user);
}
return user;
}
}
建议配置:
yaml复制app:
cache:
cache-null-values: true # 缓存空值防止穿透
java复制CacheDef.of("importantData", Duration.ofMinutes(30).plusSeconds(random.nextInt(300)));
通过给TTL添加随机值,避免大量缓存同时过期。
确保在请求入口处设置租户上下文:
java复制@Component
@RequiredArgsConstructor
public class TenantFilter implements Filter {
private final TenantContext context;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String tenantId = extractTenantId(request);
context.setTenantId(tenantId);
try {
chain.doFilter(request, response);
} finally {
context.clear();
}
}
}
java复制Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats() // 开启统计
.build();
监控命中率:
java复制CacheStats stats = cache.stats();
double hitRate = stats.hitRate();
可以通过装饰器模式实现多级缓存:
java复制public class MultiLevelCache implements Cache {
private final Cache localCache;
private final Cache remoteCache;
public Object get(Object key) {
Object value = localCache.get(key);
if (value == null) {
value = remoteCache.get(key);
if (value != null) {
localCache.put(key, value);
}
}
return value;
}
}
集成Micrometer实现监控:
java复制@Bean
public CaffeineCacheManager caffeineCacheManager(/*...*/) {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCacheSpecification("...");
manager.setCacheNames(cacheNames);
manager.setMetricsEnabled(true); // 开启指标
return manager;
}
解决方案:
预防措施:
解决方案:
在商品详情页缓存中:
java复制public ProductDetail getProductDetail(String productId) {
String cacheKey = "product:" + productId;
ProductDetail detail = cacheManager.getCache("product").get(cacheKey, ProductDetail.class);
if (detail == null) {
detail = productService.getDetail(productId);
cacheManager.getCache("product").put(cacheKey, detail);
}
return detail;
}
用户关系缓存设计:
java复制public List<User> getFriends(String userId) {
String cacheKey = "friends:" + userId;
return cacheHelper.getAllValues(cacheKey, User.class);
}
java复制@Test
public void testCacheSwitch() {
// 测试LOCAL模式
environment.setProperty("app.cache.type", "LOCAL");
testCacheOperations();
// 测试REDIS模式
environment.setProperty("app.cache.type", "REDIS");
testCacheOperations();
}
使用JMeter测试:
Redis部署示例:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 3
template:
spec:
containers:
- name: redis
image: redis:6.2
ports:
- containerPort: 6379
resources:
limits:
memory: 1Gi
Spring Boot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics
health:
redis:
enabled: true
建议记录:
关键指标:
迁移步骤:
可能的扩展方向:
在实际项目中应用这套架构后,我们获得了以下收益:
几个特别值得注意的经验:
这套架构已经在我们的生产环境稳定运行超过一年,支撑了日均千万级的访问量。特别是在应对大促活动时,能够通过简单的配置调整快速适应流量变化,体现了良好的弹性设计。