taobao-pamirs-proxycache源码分析学习与修改

原创
2015/09/07 16:01
阅读数 1.8K

###taobao-pamirs-proxycache源码分析学习

  • 最近,由于公司业务量增长,对数据库的压力比较大,需要一款框架缓存查询结果,找到了淘宝的开源框架pamirs-proxycache,于是将其源码改改,删掉一些用不到的功能,增加一些自己的功能,成为公司框架,记录下项目的开发思路和遇到的问题供今后参考

由于业务增长,导致数据库压力大,所以考虑将一些数据缓存,这些数据如一些系统的配置数据、历史的订单数据、一些模板数据等,这些数据都经常被使用并且基本不被修改

####需求说明 使用此框架的原因和解决的问题以及注意点如下:

  1. 系统采用分布式,不同系统可能对同一记录进行修改,缓存范围为集群范围。
  2. 缓存已经确认使用memcached,由于已经在项目中成熟使用,不建议更换ehcache等其他缓存框架
  3. 如果缓存框架一旦出问题,需要随时将框架从系统移除,对业务侵入小
  4. 数据库使用ibitas框架,但是ibitas的缓存更新机制不能很好的控制,开发人员不小心则可能会造成脏数据
  5. 缓存的数据使用key-val方式存储
  6. 项目集成spring框架,可以利用spring的aop功能

整体的设计思路很简单,只需要在对应的dao方法或者service方法之后加上aop,组装key并将执行的查询结果保存进memcached。整个项目的关键都在于这个缓存key的组装,考虑过两种方式,

  1. 使用返回对象的主键作为key
  2. 使用包名+bean名+方法名+参数作为key

两种各有优缺点,

  • 如果使用第一种,只需要在domain的类中利用注解的方式进行简单配置,在另外的xml文件中,则进行简单配置即可,但是这种方式对于对列表返回不太友好,并非以主键作为唯一查询条件的对象(比如订单有时会根据商家号+商家订单号查询,有时候会根据ID查询)不太友好。以订单为例,缓存中保存以订单主键和商家号+商家订单号两种key的对象,如果对该记录进行更新,则需要找出更新方法更新的主键和商家号+商家订单号,那么类似的,必须按照一定规则找出所有可能存在的key,考虑到这个数据结构和算法的设计比较复,在集群中可能效率还不如直接访问数据库,所以暂时抛弃这种方式,但是如果只是需要以主键作为key,那么这种方案肯定是最好实现的。
  • 第二种方式,则不考虑返回值为何,key只关心方法和参数,缓存其结果

####框架下载 框架下载地址taobao-pamirs-proxycache,下载下来后,发现报错,原因是此框架自带了淘宝另一个淘宝开发的缓存项目tair,下载下来,tair报错不用管它,此时taobao-pamirs-proxycache所依赖的项目能找到,不报错了,就可以安心的研究源码了。官方wiki给了一些项目简单介绍,写的不是很详细,只能作为开发者自己的一个记录,不能作为其他参考和打算使用此项目的人提供太大帮助。项目结构图如下:

src/main/java/
	   com.taobao.pamirs.cache
		-extend *扩展功能,jmx监控,日志打印等*
		-framework *项目核心部分,包括核心aop部分和配置以及定时器部分*
			--aop
			--config
			--listener
			--timer
		-load *加载时一些操作类*
			--impl
			--varify
		-store *本地存储*
		-util *工具类*
		-CacheManager.java *框架管理类*
src/main/resources
		-designmodel *配置示例*
		-extend.jmx *jmx配置*
		-load *主要缓存和spring配置*

其中,扩展的功能和淘宝tair以及定时清除缓存的功能都用不上,所以暂时没有研究的价值,也就在自己项目中将其去除,

####原框架思路解析

#####项目关键运行思路如下:

  1. src/main/resources.load下的cache-spring.xml项目启动时被加载,初始化com.taobao.pamirs.cache.load.impl.LocalConfigCacheManager类,此类继承自AbstractCacheConfigService,利用模板方法思想,在LocalConfigCacheManager中实现读取配置文件方法,此方法中读取到配置的src/main/resources.load/cache-config.xml文件利用XStream转换为与之对应的com.taobao.pamirs.cache.framework.config下的对象。
  2. 所有的bean通过com.nbtv.proxycache.aop.handle.CacheManagerHandle的getAdvicesAndAdvisorsForBean方法,判断是否在步骤1中有其bean的配置,如果有,则新建并返回其代理类,若无,则返回空。
  3. 系统启动完成后调用com.nbtv.proxycache.CacheManager的onApplicationEvent方法,此方法进行配置的自动填充,配置的校验等初始化操作
  4. 当配置的bean以及方法被调用时,实际上被调用的是bean的代理方法,即com.nbtv.proxycache.aop.advice.CacheManagerRoundAdvice的invoke方法,此方法中,判断当前方法和参数是否在步骤一配置的方法中。
    • 如果此时方法在配置的方法中并且为记录缓存方法,则根据bean、方法、参数组装成缓存key,并尝试从缓存中获取该key的对象,如果获取到,则返回获取到的对象,否则继续
    • 如果未从缓存获取到对象,则走原生方法从数据库获取对象,并且将结果保存到缓存中
    • 如果当前方法在配置的方法中,并且为删除缓存方法,则组装所有需要删除的key,并从缓存中删除remove掉对应的记录
  5. 流程结束。

#####关键步骤代码 项目的几个关键步骤,用到了spring处理aop的一些接口和类,这里拿出来做下说明,

  1. com.nbtv.proxycache.CacheManager,此类对配置进行填充和校验,结构如下 public abstract class CacheManager implements ApplicationContextAware,ApplicationListener{ protected ApplicationContext applicationContext; @Override public void onApplicationEvent(ApplicationEvent event) { //实现ApplicationListener方法,当ApplicationContext执行了applicationContext.publishEvent(event)方法后,会自动通知所有实现了ApplicationListener的对象的onApplicationEvent方法,onApplicationEvent方法中判断如果事件是所监听的事件,则进行相应的处理 } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { //此类实现了ApplicationContextAware方法,方便取到applicationContext对象 this.applicationContext = applicationContext; } }
  2. com.nbtv.proxycache.aop.handle.CacheManagerHandle,此类为需要代理的bean创建代理对象,结构如下 public class CacheManagerHandle extends AbstractAutoProxyCreator { @Override protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, TargetSource targetSource) throws BeansException { //实现了AbstractAutoProxyCreator的方法,此类模仿spring自带的自动代理类BeanNameAutoProxyCreator,bean被加载后,会通过此方法,如果bean在配置中,则创建对应的代理类,否则不做处理。 if (ConfigUtil.isBeanHaveCache(cacheManager.getCacheConfig(), beanName)) { return new CacheManagerAdvisor[] { new CacheManagerAdvisor(cacheManager, beanName) }; } return DO_NOT_PROXY; } }
  3. com.nbtv.proxycache.aop.advice.CacheManagerRoundAdvice,真正的代理方法,执行对缓存的操作工作,结构如下 public class CacheManagerRoundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //实现了MethodInterceptor的方法,此方法即目标bean的代理处理方法 //处理缓存方法 } }

#####项目启动时spring所做的操作如下:

  1. 系统启动构造ClassPathXmlApplicationContext对象
  2. 调用org.springframework.context.support. ClassPathXmlApplicationContext 的ClassPathXmlApplicationContext执行refresh()操作;(refresh方法对beanFactory进程一系列操作,并对工厂bean、监听bean、特殊bean等进行初始化)。
  3. Refresh()方法中执行registerBeanPostProcessors()方法
    1. 此方法如果当前bean是所配置的缓存加载bean CacheManage,则执行其init方法,读取配置文件。详见其继承类LocalConfigCacheManager. loadConfig()方法
  4. Refresh()方法中执行finishBeanFactoryInitialization(beanFactory)方法对其他普通bean进行处理
    1. finishBeanFactoryInitialization方法调用org.springframework.beans.factory.support. DefaultListableBeanFactory的preInstantiateSingletons(),循环所有bean的名称,调用getBean(beanName)方法,对bean初始化。
    2. 调用org.springframework.beans.factory.support. AbstractBeanFactory的doGetBean方法,此方法先做一些校验操作,然后调用getSingleton()方法,构造单例对象
    3. 调用org.springframework.beans.factory.support. AbstractAutowireCapableBeanFactory的createBean方法此方法先尝试构造前置代理,如果失败,则调用doCreateBean方法创建bean
    4. 之后调用到initializeBean方法,先初始化bean,
    5. 然后调用applyBeanPostProcessorsAfterInitialization()方法构造bean的处理器
    6. applyBeanPostProcessorsAfterInitialization调用其父类的getBeanPostProcessors()方法,获取到所有继承了AbstractAutoProxyCreator类的类,遍历所有类并执行其postProcessAfterInitialization方法
    7. 执行com.nbtv.proxycache.aop.handle. CacheManagerHandle的getAdvicesAndAdvisorsForBean方法,判断当前的bean是否在缓存代理配置文件中配置了需要代理,如果需要,则创建一个引介切面,并返回该引介切面
    8. 引介切面作为参数,调用AbstractAutoProxyCreator. wrapIfNecessary中调用createProxy方法,构建代理对象,并返回。
    9. 代理对象为jdkDynamicAopProxy,
  5. Refresh()方法中执行finishRefresh()方法进行收尾操作
    1. finishRefresh()方法调用publishEvent,发布ContextRefreshedEvent事件,并广播该事件
    2. CacheManager. onApplicationEvent监听到ContextRefreshedEvent事件,对配置的cacheBean分别进行填充和静态校验,并初始化缓存,详细实现请看代码
  6. 至此,已经初始化了所有的bean,并且根据配置构好了代理的对象

#####系统运行时流程如下(以service调用Dao,dao被配置了缓存代理为例):

  1. Service中获取到的memberDao对象,此对象实际上是原Dao的代理对象$proxy
  2. $proxy执行代理的前置方法,对缓存进行操作(组装key,并且添加或者删除缓存,具体执行流程请查看CacheManagerRoundAdvice. Invoke()方法)
  3. 如果是删除缓存方法,则会继续调用invocation.proceed(),尝试执行接下来的代理方法或者原生方法
  4. 如果有其他代理,则找到了下一个代理并执行相应方法,
  5. 调用原生方法

####项目新增和修改过的功能:

  1. 缓存bean和删除缓存方法都加上了<prefix></prefix>用来防止不同的集群项目使用到相同的bean
  2. methodConfig中增加<cache></cache>配置,允许不同的缓存方法是用不同的缓存bean,
  3. 参数类型不参与缓存key的组成,因为参数的值已经可以满足key的唯一性,加上类型会使key增长
  4. 增加<parametersIndexs></parametersIndexs>标签,为参与缓存key的参数顺序,比如有三个参数,但是只有第一个和第二个参与key,并且第二个参数是个对象,对象中的userName参与key组装,则配置为1,2#userName
  5. 对删除缓存方法的引用方法去除bean的正确性校验,由于集群,可能引用方法并不在本项目中,校验肯定是失败的。
  6. 缓存修改支持使用不同的cache,只需要在配置中配置不同的cache名即可

修改后的项目,更能适应集群环境 缓存代理配置文件修改如下

<?xml version="1.0" encoding="GBK"?>
<cacheConfig>
	<!--
		缓存配置示例,此配置保存至缓存的key为:prefix#参数一@参数二
	-->
	<!-- 需要添加到缓存的bean的集合 -->
	<cacheBeans>
		<cacheBean>
			<!-- 需要缓存返回值的bean名 -->		
			<beanName>userDao</beanName>	
			<!-- 需要缓存返回值的方法集合 -->
			<cacheMethods>
				<!-- 方法,可以有多个 -->
				<methodConfig>
					<!-- 添加至缓存的对象的前缀,无限制,推荐以方法返回对象的包名+类名,长度不宜太长,
					如果返回类型为基本数据类型,最好根据功能命名为特殊的并且系统唯一的前缀 -->
					<prefix>com.test.User</prefix>		
					<!-- 方法名 -->
					<methodName>findUserById</methodName>
					<!-- 超时时间,可选,不配置采用缓存默认配置(memcached默认配置为永久有效) -->
					<expiredTime>10</expiredTime>
					<!-- 方法参数,如果有重载方法时,必须要指定,可选 -->
					<parameterTypes>
						<java-class>java.lang.String</java-class>
					</parameterTypes>
					<!-- 参与组装key的参数位置,可选,2#memberId#memberName,3:第二个参数的memerId值和memberName值和第三个参数参与组装key -->
					<parameterIndex>1</parameterIndex>
					<!-- 使用的缓存bean名称,可以配置不同缓存 ,可选-->
					<cache>cache</cache>
				</methodConfig>
			</cacheMethods>
		</cacheBean>
	</cacheBeans>
	<cacheCleanBeans>	<!-- 需要执行删除缓存操作的bean的集合 -->
		<!-- 需要执行删除缓存操作的bean名称,可以配置多个 -->
		<cacheCleanBean>
			<!-- 需要执行删除缓存操作的bean名称 -->
			<beanName>userDao</beanName>	
			<!-- 需要执行删除缓存操作的方法列表 -->
			<methods>
				<!-- 删除缓存操作方法 -->
				<cacheCleanMethod>					
					<!-- 方法名 -->
					<methodName>updateUserById</methodName>
					<!-- 对应删除删除的bean列表-->
					<cleanBeans>
						<!-- bean -->
						<cleanBean>
							<!-- 删除前缀,与 cacheBeans中前缀对应-->
							<prefix>com.test.User</prefix>	
							<!-- 使用的缓存,与 cacheBeans中使用缓存对应 -->
							<cache>cache</cache>
						</cleanBean>
					</cleanBeans>
				</cacheCleanMethod>
			</methods>
		</cacheCleanBean>
	</cacheCleanBeans>
</cacheConfig>

后记:此项目虽然是一个比较简单的功能,但是项目的作者考虑的非常周全,功能比较齐全,代码非常规范,思路非常清晰,并且大量用到了设计模式,可以看出作者的功底非常好。读此项目,受益匪浅,向作者致敬。

展开阅读全文
打赏
3
68 收藏
分享
加载中
是为了删除缓存简单?
2016/01/31 21:50
回复
举报
在咨询个问题 methodConfig中增加<cache></cache>配置,允许不同的缓存方法是用不同的缓存bean 这个是什么作用?
你们使用的key 最终是 region@prefix#cache#beanName#methodName#value1。。。 ?
2016/01/31 21:48
回复
举报

引用来自“dzhai”的评论

能开源下扩展的代码吗 thanks
347112281@qq.com

引用来自“jason-寒江雪”的评论

目前公司项目中已经在使用,但是由于框架是为公司开发的,涉及到公司层面的东西了,目前暂时不会开源,如果开源,我会将项目放到git上
额,那还是我自己改吧
2016/01/31 19:23
回复
举报

引用来自“dzhai”的评论

能开源下扩展的代码吗 thanks
347112281@qq.com
目前公司项目中已经在使用,但是由于框架是为公司开发的,涉及到公司层面的东西了,目前暂时不会开源,如果开源,我会将项目放到git上
2016/01/28 09:35
回复
举报
能开源下扩展的代码吗 thanks
347112281@qq.com
2016/01/27 20:53
回复
举报
更多评论
打赏
5 评论
68 收藏
3
分享
在线直播报名
返回顶部
顶部