taobao-pamirs-proxycache開源快取代理框架實現原理剖析
寫在前面
taobao-pamirs-proxycache 是一款開源快取代理框架, 它將 快取程式碼 與 業務程式碼 解耦。讓開發專注coding業務, 快取通過xml配置即可實現。本文先從此工具如何使用講起,給大家帶來點感知~再從原始碼剖析它的實現原理。
一、proxycache工具的感知
1.1 使用場景
假設我有這樣的一個場景,在訪問UserWhiteReadService.getUserWhiteByAppAndWhiteCode時,希望先從快取獲取,結果為空,則走原生方法,再把原生方法返回的結果put到快取。傳統的做法,會寫一堆取快取再判空等程式碼。方法多了的話,每個要快取的方法需要重複上述coding。結合這種場景,使用taobao-pamirs-proxycache 能給我們帶來什麼好處。從下面的程式碼來看,業務程式碼中去除了快取的相關程式碼。只需要配置下xml即可達到傳統做法的目的。管理更加集中了。
public ResultSupport<List<UserWhiteEventDTO>> getUserWhiteByAppAndWhiteCode(String appName, String userWhiteCode) throws Exception { ResultSupport<List<UserWhiteEventDTO>> res = new ResultSupport<List<UserWhiteEventDTO>>(); try { List<UserWhiteEventDO> r = userWhiteEventDAO.selectUserWhitesByAppAndWhiteCode(appName, userWhiteCode); res.setModule(TransferUtils.convert2UserWhiteEventDTOList(r)); res.setSuccess(Boolean.TRUE); } catch (Exception e) { res.setMessage("異常 : " + e); throw new Exception("UserWhiteReadServiceImpl.getUserWhiteByAppAndWhiteCode error : " + e); } return res; }
快取、清理方法配置 biz-cache.xml
<?xml version="1.0" encoding="gb2312"?> <cacheModule> <!-- 快取bean list --> <cacheBeans> <cacheBean> <beanName>userWhiteReadService</beanName> <cacheMethods> <methodConfig> <methodName>getUserWhiteByAppAndWhiteCode</methodName> <expiredTime>2592000</expiredTime><!-- 指定快取生命週期 --> </methodConfig> <methodConfig> <methodName>getUserWhitesByUserId</methodName> <expiredTime>2592000</expiredTime><!-- 指定快取生命週期 --> </methodConfig> </cacheMethods> </cacheBean> </cacheBeans> <!-- 清快取bean list --> <cacheCleanBeans> <cacheCleanBean> <beanName>userWhiteReadService</beanName> <methods> <cacheCleanMethod> <methodName>cleanByAppAndCode</methodName> <cleanMethods> <methodConfig> <methodName>getUserWhiteByAppAndWhiteCode</methodName> </methodConfig> </cleanMethods> </cacheCleanMethod> <cacheCleanMethod> <methodName>cleanByUserId</methodName> <cleanMethods> <methodConfig> <methodName>getUserWhitesByUserId</methodName> </methodConfig> </cleanMethods> </cacheCleanMethod> </methods> </cacheCleanBean> </cacheCleanBeans> </cacheModule>
cache配置 base-cache.xml
<?xml version="1.0" encoding="gb2312"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
default-autowire="byName">
<bean id="tairManager" class="com.taobao.tair.impl.mc.MultiClusterTairManager"
init-method="init">
<property name="configID">
<value>${tair.configID}</value>
</property>
<property name="dynamicConfig">
<value type="java.lang.Boolean">true</value>
</property>
</bean>
<bean id="cacheManager" class="com.taobao.pamirs.cache.load.impl.LocalConfigCacheManager"
init-method="init" depends-on="tairManager">
<property name="storeType" value="tair" />
<property name="tairNameSpace" value="${tair.namespace}" /><!-- 快取tair空間 -->
<property name="storeRegion" value="${tair.store.region}" /> <!-- 快取環境隔離 -->
<property name="configFilePaths">
<list>
<value>spring/cache/biz-cache.xml</value>
</list>
</property>
<property name="tairManager" ref="tairManager" />
</bean>
<bean class="com.taobao.pamirs.cache.framework.aop.handle.CacheManagerHandle">
<property name="cacheManager" ref="cacheManager" />
</bean>
</beans>
二、proxy-cache 框架模組
快取配置資訊載入模組
beanProxy(bean代理物件)生成模組
CacheProxy(快取代理物件)生成模組
日誌監控模組(本文不講)
三、實現原理
3.1 快取配置資訊載入架構圖
從上圖及結合原始碼, CacheManager 是快取框架的載入入口。CacheManager 有兩個關鍵實現細節 :
1、定義了初始化方法init( ), 由子類LocalConfigCacheManager實現loadConfig( )。這是載入快取配置資訊,組裝成快取元件的入口。
2、實現了ApplicationListener 介面,重寫了監聽事件方法。
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent) {
// 2. 自動填充預設的配置
autoFillCacheConfig(cacheConfig);
// 3. 快取配置合法性校驗
verifyCacheConfig(cacheConfig);
// 4. 初始化快取
initCache();
}}
initCache()方法, 主要是對快取適配key的構造、生成所有需快取的方法對應的"快取代理" -- CacheProxy, 及快取的定時清理任務。下面對上述各個細節點一一講解。
3.1.1快取介面卡key的構造
public static String getCacheAdapterKey(String region, String beanName,
MethodConfig methodConfig) {
Assert.notNull(methodConfig);
// 最終的key
StringBuilder key = new StringBuilder();
// 1. region
if (StringUtils.isNotBlank(region))
key.append(region).append(REGION_SPLITE_SIGN); // "@"
// 2. bean + method + parameter
String methodName = methodConfig.getMethodName();
List<Class<?>> parameterTypes = methodConfig.getParameterTypes();
key.append(beanName).append(KEY_SPLITE_SIGN); // "#"
key.append(methodName).append(KEY_SPLITE_SIGN); // "#"
key.append(parameterTypesToString(parameterTypes));
return key.toString();
}
3.1.2 快取處理適配CacheProxy的組裝
CacheProxy :包含了介面卡Key、快取型別(如 tair快取 or Map本地快取)、 快取對應的物件bean及method、快取空間(tair要用到)等。
ICache : 則是快取基礎介面。提供了get 、 put、clean等通用方法。目前支援tair 、 Map本地 兩種快取型別
3.2 beanProxy 代理物件生成結構圖
CacheManagerHandle : 這個快取處理類很關鍵,它實現了AbstractAutoProxyCreator介面,重寫了getAdvicesAndAdvisorsForBean方法,實現了自己的AOP切面CacheManagerAdvisor。CacheManagerAdvisor,依賴了CacheManagerRoundAdvice攔截器, CacheManagerRoundAdvice 通過實現 MethodInterceptor介面的invoke 方法,實現了在訪問目標方法時植入快取訪問、清快取切面 。具體可以看下下面這一小段原始碼 :
protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass,
String beanName, TargetSource targetSource) throws BeansException {
log.debug("CacheManagerHandle in:" + beanName);
if (ConfigUtil.isBeanHaveCache(cacheManager.getCacheConfig(), beanName)) {
log.warn("CacheManager start... ProxyBean:" + beanName);
return new CacheManagerAdvisor[] { new CacheManagerAdvisor(
cacheManager, beanName) };
}
return DO_NOT_PROXY;
}
CacheManagerRoundAdvice 重寫的invoke方法 : 訪問目標方法前進行攔截,如果是訪問快取的操作, 則植入快取代理切面,優先從快取結果中取,取不到再從原生方法取資料,並且put 到 快取。 如果是清理快取的操作, 則在原生方法訪問後,清理原生方法歷史快取資料。
public Object invoke(MethodInvocation invocation) throws Throwable {
MethodConfig cacheMethod = null;
List<MethodConfig> cacheCleanMethods = null;
String storeRegion = "";
Method method = invocation.getMethod();
String methodName = method.getName();
try {
CacheConfig cacheConfig = cacheManager.getCacheConfig();
storeRegion = cacheConfig.getStoreRegion();
List<Class<?>> parameterTypes = Arrays.asList(method
.getParameterTypes());
cacheMethod = ConfigUtil.getCacheMethod(cacheConfig, beanName,
methodName, parameterTypes);
cacheCleanMethods = ConfigUtil.getCacheCleanMethods(cacheConfig,
beanName, methodName, parameterTypes);
} catch (Exception e) {
log.error("CacheManager:切面解析配置出錯:" + beanName + "#"
+ invocation.getMethod().getName(), e);
return invocation.proceed();
}
String fromHsfIp = "";// hsf consumer ip
try {
fromHsfIp = (String) invocation.getThis().getClass()
.getMethod("getCustomIp").invoke(invocation.getThis());
} catch (NoSuchMethodException e) {
log.debug("介面沒有實現HSF的getCustomIp方法,取不到Consumer IP, beanName="
+ beanName);
}
try {
// 1. 走快取
if (cacheManager.isUseCache() && cacheMethod != null) {
String adapterKey = CacheCodeUtil.getCacheAdapterKey(
storeRegion, beanName, cacheMethod);
CacheProxy<Serializable, Serializable> cacheAdapter = cacheManager
.getCacheProxy(adapterKey);
String cacheCode = CacheCodeUtil.getCacheCode(storeRegion,
beanName, cacheMethod, invocation.getArguments());
return useCache(cacheAdapter, cacheCode,
cacheMethod.getExpiredTime(), invocation, fromHsfIp);
}
// 2. 清理快取
if (cacheCleanMethods != null) {
try {
return invocation.proceed();
} finally {
cleanCache(beanName, cacheCleanMethods, invocation,
storeRegion, fromHsfIp);
}
}
// 3. 走原生方法
return invocation.proceed();
} catch (Exception e) {
// log.error("CacheManager:出錯:" + beanName + "#"
// + invocation.getMethod().getName(), e);
throw e;
}
}
四、那些踩過的坑
原生方法,不要隨意捕獲異常;或者在捕獲異常後,要手動throw異常出來。因為使用了該快取工具,只要呼叫此方法不丟擲異常,原生方法的結果(不排除異常結果)會被框架快取住。記得有一次在斷網演練的時候,由於斷網導致連線DB出問題,異常資訊還是被我catch掉了,結果就悲劇了,異常資訊結果被快取住了。導致應用恢復時,再次呼叫此方法,返回的結果一直都是exception~
寫在最後
我的新部落格
CSDN部落格經常打不開, 老部落格繼續維護一段時間吧~~