1. 程式人生 > >taobao-pamirs-proxycache開源快取代理框架實現原理剖析

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 快取配置資訊載入架構圖

pic

從上圖及結合原始碼, 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本地 兩種快取型別

pic

3.2 beanProxy 代理物件生成結構圖

pic

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部落格經常打不開, 老部落格繼續維護一段時間吧~~