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

spring cache 的概念

Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,经过在既有代码中添加少许它定义的各类 annotation,即可以达到缓存方法的返回对象的效果。git

  • @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 工具中,会经过':'的分隔符,进行分组,展现效果会更好redis

增长 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());
    }

总结

相关文章
相关标签/搜索