扩展spring cache 支持缓存多租户及其自动过期

39次阅读

共计 3270 个字符,预计需要花费 9 分钟才能阅读完成。

spring cache 的概念
Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
@Cacheable 使用效果 , 更具 cacheName(value) + 请求入参(key)组成保存 redis 中的 key
public class PigxClientDetailsService extends JdbcClientDetailsService {
@Cacheable(value = SecurityConstants.CLIENT_DETAILS_KEY, key = “#clientId”)
public ClientDetails loadClientByClientId(String clientId) {
return super.loadClientByClientId(clientId);
}
}}

多租户下缓存问题分析

默认情况 A 租户入参为 K1 请求 应用,spring cache 会自动缓存 K1 的值,如果 B 租户 入参同时为 K1 请求应用时,spring cache 还是会自动关联到同一个 Redis K1 上边查询数据。
在多租户下 A/B 租户所请求的 K1 并不是同一入参(虽然看起来参数名 参数值都是一样的),更不能返回同一个结果。
默认的 spring cache 根据入参来区分 不能满足多租户系统的设计需求, 不能实现根据租户隔离。

区分缓存增加租户标识

A 租户入参为 K1,spring cache 维护 Redis Key 在拼接一个租户信息
KEY = cacheName + 入参 + 租户标识
这样 A /B 租户请求参数相同时,读取的也是不同的 Key 里面的值,避免数据脏读,保证隔离型

重写 Spring Cache 的 cacheManager 缓存管理器
从上下文中获取租户 ID,重写 @Cacheable value 值即可完成,然后注入这个 cacheManager
@Slf4j
public class RedisAutoCacheManager extends RedisCacheManager {
/**
* 从上下文中获取租户 ID,重写 @Cacheable value 值
* @param name
* @return
*/
@Override
public Cache getCache(String name) {
return super.getCache(TenantContextHolder.getTenantId() + StrUtil.COLON + name);
}
}
为什么要用 StrUtil.COLON 即 ‘:’ 分割
在 GUI 工具中,会通过 ’:’ 的分隔符,进行分组,展示效果会更好
增加 spring cache 的主动过期功能
默认的注解里面没有关于时间的入参,如下图
public @interface Cacheable {

@AliasFor(“cacheNames”)
String[] value() default {};

@AliasFor(“value”)
String[] cacheNames() default {};

String key() default “”;

String keyGenerator() default “”;

String cacheManager() default “”;

String cacheResolver() default “”;

String condition() default “”;

String unless() default “”;

boolean sync() default false;

}

还是以 value 作为入口 value = “menu_details#2000” 通过对 vaue 追加一个数字 并通过特殊字符分割,作为过期时间入参
@Service
@AllArgsConstructor
public class PigXMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
private final SysRoleMenuMapper sysRoleMenuMapper;

@Override
@Cacheable(value = “menu_details#2000”, key = “#roleId + ‘_menu'”)
public List<MenuVO> findMenuByRoleId(Integer roleId) {
return baseMapper.listMenusByRoleId(roleId);
}
}
重写 cachemanager 另个重要的方法 创建缓存的方法, 通过截取 value 中设置的过期时间,赋值给你 RedisCacheConfiguration
public class RedisAutoCacheManager extends RedisCacheManager {
private static final String SPLIT_FLAG = “#”;
private static final int CACHE_LENGTH = 2;

@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) {
return super.createRedisCache(name, cacheConfig);
}

String[] cacheArray = name.split(SPLIT_FLAG);
if (cacheArray.length < CACHE_LENGTH) {
return super.createRedisCache(name, cacheConfig);
}

if (cacheConfig != null) {
long cacheAge = Long.parseLong(cacheArray[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(cacheAge));
}
return super.createRedisCache(name, cacheConfig);
}
}
spring cache 操作缓存时 获取到上步设置的 ttl 赋值给 key
@Override
public void put(Object key, @Nullable Object value) {

Object cacheValue = preProcessCacheValue(value);

if (!isAllowNullValues() && cacheValue == null) {

throw new IllegalArgumentException(String.format(
“Cache ‘%s’ does not allow ‘null’ values. Avoid storing null via ‘@Cacheable(unless=\”#result == null\”)’ or configure RedisCache to allow ‘null’ via RedisCacheConfiguration.”,
name));
}

cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
总结

通过对 spring cache 的扩展即可实现对缓存 一些透明操作
cachemanager 是 springcache 对外提供的 API 扩展入口
以上源码参考个人项目 基于 Spring Cloud、OAuth2.0 开发基于 Vue 前后分离的开发平台

QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。
欢迎关注我们的公众号获得更多的好玩 JavaEE 实践

正文完
 0