taobao-pamirs-proxycache源碼分析學習與修改

###taobao-pamirs-proxycache源碼分析學習

  • 最近,因爲公司業務量增加,對數據庫的壓力比較大,須要一款框架緩存查詢結果,找到了淘寶的開源框架pamirs-proxycache,因而將其源碼改改,刪掉一些用不到的功能,增長一些本身的功能,成爲公司框架,記錄下項目的開發思路和遇到的問題供從此參考

因爲業務增加,致使數據庫壓力大,因此考慮將一些數據緩存,這些數據如一些系統的配置數據、歷史的訂單數據、一些模板數據等,這些數據都常常被使用而且基本不被修改java

####需求說明 使用此框架的緣由和解決的問題以及注意點以下:算法

  1. 系統採用分佈式,不一樣系統可能對同一記錄進行修改,緩存範圍爲集羣範圍。
  2. 緩存已經確認使用memcached,因爲已經在項目中成熟使用,不建議更換ehcache等其餘緩存框架
  3. 若是緩存框架一旦出問題,須要隨時將框架從系統移除,對業務侵入小
  4. 數據庫使用ibitas框架,可是ibitas的緩存更新機制不能很好的控制,開發人員不當心則可能會形成髒數據
  5. 緩存的數據使用key-val方式存儲
  6. 項目集成spring框架,能夠利用spring的aop功能

總體的設計思路很簡單,只須要在對應的dao方法或者service方法以後加上aop,組裝key並將執行的查詢結果保存進memcached。整個項目的關鍵都在於這個緩存key的組裝,考慮過兩種方式,spring

  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的一些接口和類,這裏拿出來作下說明,app

  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>

後記:此項目雖然是一個比較簡單的功能,可是項目的做者考慮的很是周全,功能比較齊全,代碼很是規範,思路很是清晰,而且大量用到了設計模式,能夠看出做者的功底很是好。讀此項目,受益不淺,向做者致敬。

相關文章
相關標籤/搜索