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 實踐