效率
同一次業務操作過程中,往往會出現某種操作被重復執行,邏輯上來講如果只執行一次是最理想的。這里所指的操作特指一些IO操作,比如從數據庫中獲取登錄人的信息,也就是說如果一次請求中包含5個小邏輯,這5個小邏輯包含3次獲取用戶信息的操作,理想的情況是3次只有一次是從數據庫中加載,其余的兩次從緩存中獲取。
- 多次調用,每個服務實現獨自請求用戶信息。
- 一次調用,多次讀取。首先從Context中加載,如果失敗從數據庫中加載,最后將結果存入Context。
前提
限於非web環境,這里是dubbo實現的微服務。如果是web環境的話解決問題比較簡單,因為我們可以充分利用Spring Framwork中提到的三個bean生命周期的特殊來解決:
- request
- session
- global
案例
將老的價格數據遷移成新的價格數據,這里大概是如下的步驟:
- 刪除新老價格的關系,因為需要支持重復遷移
- 禁用之前已經存在的價格規則,規則是描述價格在某種場景下生效的邏輯
- 創建新的價格
- 創建新的規則
- 啟用新規則
上面步驟的價格,規則,關系數據分別屬於三個業務對象,自身都具備CRUD的服務接口,這些CRUD都需要記錄操作人信息,記錄的標准就是接口傳入的操作人所持有的token,我們需要將這個token轉換成userId,userName之類的信息與價格,規則等信息一並存儲。
時序圖如下:
問題:遷移一條價格多次讀取用戶信息效率低
由於遷移價格會涉及到多個對象的操作,而操作這些具體業務對象的接口並不支持傳具體的userId,userName只支持token,所以不可避免的會在保存價格等信息時各自去根據token查詢操作人信息。實測一個價格完成一次數據遷移涉及到獲取用戶信息的次數多達20+次,效率是比較低,如何去解決呢?
現狀
由於我目前實現的微服務是無狀態的,也不是web環境,所以上面提到的那些bean的作用域功能就使用不上。
目標
實現類似request作用域的功能,一次請求僅執行一次,其余的請求從緩存中獲取結果以提高IO操作效率。
方案
可采用TreadLocal來當緩存,存儲頻繁讀取的數據。增加了CacheContext,獲取用戶首先從CacheContext中取,如果為空則從數據庫加載然后回寫到Treadlocal中,下一次再請求用戶信息時就可以命中緩存不需要再次從數據庫中加載,顯然效率得到了質的提升。
時序圖如下:
實現步驟如下:
- 創建Context數據對象
緩存的容器為TreadLocal,可以定義多個需要緩存的屬性,並提供一個清除所有緩存的方法。清除的方法是必做的,否則會一直停留在線程中,當線程被再次利用時會獲取到上一次請求存儲的數據。
@Service
public class ProductContext {
private static Logger logger = Logger.getLogger(CiaServiceImpl.class);
private ThreadLocal<CiaUserInfo> ciaUserInfoThreadLocal=new ThreadLocal<>();
private void clearCiaUserInfo(){
this.ciaUserInfoThreadLocal.remove();
this.logger.info("清除getTokenInfo緩存成功");
}
public void setCiaUserInfoToCache(CiaUserInfo ciaUserInfo){
this.clearCiaUserInfo();
this.ciaUserInfoThreadLocal.set(ciaUserInfo);
this.logger.info("將getTokenInfo存儲到緩存中");
}
public CiaUserInfo getCiaUserInfoFromCache(String token){
CiaUserInfo ciaUserInfo =this.ciaUserInfoThreadLocal.get();
if(null!=ciaUserInfo){
this.logger.info("從緩存中獲取到用戶信息getTokenInfo");
}
return ciaUserInfo;
}
public void clearAll(){
this.clearCiaUserInfo();
this.logger.info("清除ProductContext的緩存成功");
}
}
- 修改獲取用戶信息的服務類,結合Context操作緩存。
public CiaUserInfo getTokenInfo(String token) throws Exception {
CiaUserInfo result = this.productContext.getCiaUserInfoFromCache(token);
if(null!=result){
return result;
}
else {
result=new CiaUserInfo();
}
//...get user from db
this.productContext.setCiaUserInfoToCache(result);
return result;
}
- 增加注解,用來標識哪些方法是需要使用緩存的
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalCacheContext {
/**
* 是否啟動
* @return
*/
boolean enable() default true;
}
- 增加攔截器
TreadLocal中存入的信息需要有效及時的釋放,配合上面申明的注解來完成。
@Aspect
public class LocalCacheContextInterceptor {
private Logger logger = LoggerFactory.getLogger(getClass().getName());
@Autowired
private ProductContext productContext;
@Pointcut("execution(* product.service.service.impl.*.*(..))")
public void pointCut() {
}
@After("pointCut()")
public void after(JoinPoint joinPoint) throws ProductServiceException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
LocalCacheContext localCacheContext= targetMethod.getAnnotation(LocalCacheContext.class);
if(null!=localCacheContext){
this.productContext.clearAll();
}
}
}
- 客戶端調用
@LocalCacheContext
public void migrationPrice(Long priceId) throws ProductServiceException {
this.migrationPriceService.migrationPrice(priceId);
}