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">
      <!-- 对应diamond上的dataid, groupid缺省为dataid-GROUP -->
      <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
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的构造

缓存适配器的key格式 : region@beanName#methodName#{String|Long}



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
pic

3.2 beanProxy 代理对象生成结构图

pic
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~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,427评论 25 707
  • 上一篇《WEB请求处理一:浏览器请求发起处理》,我们讲述了浏览器端请求发起过程,通过DNS域名解析服务器IP,并建...
    七寸知架构阅读 80,920评论 21 356
  • 文‖云飞扬 你,悄悄地来了 走出古风诗词的一联舒缓 从有你的塞北 到我在的江南 你,默默地走近 挥洒腊梅最后的一抹...
    山东云飞扬阅读 164评论 0 0