擴展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());
    }

總結

相關文章
相關標籤/搜索