1. 项目背景与核心价值
在微服务架构盛行的当下,缓存技术已经成为提升系统性能的标配组件。但实际开发中我们常常面临这样的困境:本地缓存(如Caffeine)和分布式缓存(如Redis)各有优劣,业务初期可能只需要本地缓存,随着规模扩大又不得不重构代码接入Redis。更麻烦的是在多租户场景下,如何优雅实现租户间的缓存隔离?
这个项目实现了一套Spring Boot缓存抽象层,通过极简配置即可在Caffeine和Redis之间无缝切换,同时自动处理多租户隔离问题。我在电商中台项目中落地这套方案后,缓存相关代码量减少70%,租户隔离问题彻底解决,不同环境(开发/测试/生产)可以自由选择最适合的缓存方案。
2. 架构设计与核心原理
2.1 总体架构设计
整个方案建立在Spring Cache抽象之上,通过自定义CacheManager实现缓存策略的动态路由。核心架构分为三层:
- 配置层:通过@Enable注解激活功能,支持properties/YAML配置
- 适配层:实现AbstractCacheManager,动态创建Caffeine/Redis缓存实例
- 租户层:基于ThreadLocal的租户上下文,自动注入缓存key前缀
java复制public class DynamicCacheManager extends AbstractCacheManager {
@Override
protected Cache getMissingCache(String name) {
// 根据配置决定创建Caffeine还是Redis Cache
if(config.isRedisMode()) {
return new RedisCacheWrapper(name, redisTemplate);
} else {
return new CaffeineCacheWrapper(name, caffeineBuilder);
}
}
}
2.2 多租户隔离实现
租户隔离的核心在于缓存key的自动修饰。我们通过AOP在缓存操作前自动注入租户ID:
- 使用Spring的CacheInterceptor拦截所有缓存操作
- 从ThreadLocal获取当前租户上下文
- 对原始key进行包装:
tenantId:originalKey
java复制@Aspect
@Component
public class TenantCacheAspect {
@Around("@annotation(org.springframework.cache.annotation.Cacheable)")
public Object processCacheable(ProceedingJoinPoint joinPoint) {
String tenantId = TenantContext.getCurrentTenant();
// 修改CacheOperationContext中的key生成逻辑
// ...
}
}
3. 详细配置与使用指南
3.1 基础配置示例
在application.yml中只需配置:
yaml复制cache:
strategy: redis # 可选caffeine或redis
multi-tenant: true
caffeine:
spec: maximumSize=500,expireAfterWrite=5m
redis:
ttl: 300000
keyPrefix: app_cache
3.2 代码中使用示例
业务代码完全不用关心底层实现:
java复制@Service
public class ProductService {
@Cacheable(cacheNames = "products", key = "#productId")
public Product getProduct(String productId) {
// DB查询逻辑
}
}
3.3 租户上下文设置
在拦截器或过滤器中设置租户信息:
java复制public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
TenantContext.setCurrentTenant(tenantId);
}
}
4. 性能优化与实战技巧
4.1 Caffeine调优建议
- 大小限制:根据JVM堆内存设置合理最大值
yaml复制caffeine: spec: maximumSize=10000,expireAfterAccess=10m - 权重计算:对大型对象使用weigher
java复制
Caffeine.newBuilder() .weigher((String key, Product value) -> value.getSizeInKB())
4.2 Redis优化方案
- Pipeline批量操作:对批量查询启用pipeline
- Lua脚本:复杂操作用Lua保证原子性
- 连接池配置:
yaml复制lettuce: pool: max-active: 16 max-idle: 8
4.3 混合缓存策略
对于热点数据可以采用两级缓存:
- 优先从Caffeine读取
- 未命中则查询Redis
- 仍无结果再查数据库
- 回写时同时更新两级缓存
java复制public Object getWithDoubleCache(String key) {
Object value = caffeineCache.get(key);
if(value == null) {
value = redisCache.get(key);
if(value != null) {
caffeineCache.put(key, value);
}
}
return value;
}
5. 常见问题排查指南
5.1 缓存穿透防护
问题现象:大量请求不存在的key导致DB压力
解决方案:
- 布隆过滤器前置校验
- 缓存空值(注意设置较短TTL)
java复制@Cacheable(cacheNames = "products", unless = "#result == null")
5.2 缓存一致性保障
问题场景:数据库更新后缓存未同步
推荐方案:
- 事务提交后删除缓存
java复制@Transactional @CacheEvict(cacheNames = "products", key = "#product.id") public void updateProduct(Product product) - 使用CDC工具监听binlog
5.3 内存泄漏排查
Caffeine场景内存增长过快:
- 检查是否有无限增长的缓存域
- 确认weigher计算准确
- 使用JMX监控缓存状态
java复制
caffeine.recordStats()
6. 进阶扩展方向
6.1 动态策略切换
通过Spring Cloud Config实现运行时调整:
java复制@RefreshScope
@Configuration
public class CacheConfig {
@Value("${cache.strategy}")
private String strategy;
// 配置热更新
}
6.2 多级缓存扩展
集成Ehcache实现三级缓存:
- L1:进程内Caffeine
- L2:集群内Ehcache
- L3:分布式Redis
6.3 监控集成方案
- 通过Micrometer暴露指标
java复制Caffeine.newBuilder() .recordStats() .maximumSize(1000) - Grafana监控看板配置
- 自定义缓存命中率告警
这套方案在我负责的多个项目中稳定运行,特别是在SaaS平台中,租户隔离功能完美解决了不同客户数据混存的问题。一个特别实用的技巧是:在开发环境使用Caffeine避免Redis依赖,而在CI/CD管道中通过profile自动切换为Redis进行集成测试,真正实现了一处编写处处运行。