###taobao-pamirs-proxycache源碼分析學習
- 最近,因爲公司業務量增加,對數據庫的壓力比較大,須要一款框架緩存查詢結果,找到了淘寶的開源框架pamirs-proxycache,因而將其源碼改改,刪掉一些用不到的功能,增長一些本身的功能,成爲公司框架,記錄下項目的開發思路和遇到的問題供從此參考
因爲業務增加,致使數據庫壓力大,因此考慮將一些數據緩存,這些數據如一些系統的配置數據、歷史的訂單數據、一些模板數據等,這些數據都常常被使用而且基本不被修改java
####需求說明 使用此框架的緣由和解決的問題以及注意點以下:算法
- 系統採用分佈式,不一樣系統可能對同一記錄進行修改,緩存範圍爲集羣範圍。
- 緩存已經確認使用memcached,因爲已經在項目中成熟使用,不建議更換ehcache等其餘緩存框架
- 若是緩存框架一旦出問題,須要隨時將框架從系統移除,對業務侵入小
- 數據庫使用ibitas框架,可是ibitas的緩存更新機制不能很好的控制,開發人員不當心則可能會形成髒數據
- 緩存的數據使用key-val方式存儲
- 項目集成spring框架,能夠利用spring的aop功能
總體的設計思路很簡單,只須要在對應的dao方法或者service方法以後加上aop,組裝key並將執行的查詢結果保存進memcached。整個項目的關鍵都在於這個緩存key的組裝,考慮過兩種方式,spring
- 使用返回對象的主鍵做爲key
- 使用包名+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以及定時清除緩存的功能都用不上,因此暫時沒有研究的價值,也就在本身項目中將其去除,數組
####原框架思路解析緩存
#####項目關鍵運行思路以下:數據結構
- 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下的對象。
- 全部的bean經過com.nbtv.proxycache.aop.handle.CacheManagerHandle的getAdvicesAndAdvisorsForBean方法,判斷是否在步驟1中有其bean的配置,若是有,則新建並返回其代理類,若無,則返回空。
- 系統啓動完成後調用com.nbtv.proxycache.CacheManager的onApplicationEvent方法,此方法進行配置的自動填充,配置的校驗等初始化操做
- 當配置的bean以及方法被調用時,實際上被調用的是bean的代理方法,即com.nbtv.proxycache.aop.advice.CacheManagerRoundAdvice的invoke方法,此方法中,判斷當前方法和參數是否在步驟一配置的方法中。
- 若是此時方法在配置的方法中而且爲記錄緩存方法,則根據bean、方法、參數組裝成緩存key,並嘗試從緩存中獲取該key的對象,若是獲取到,則返回獲取到的對象,不然繼續
- 若是未從緩存獲取到對象,則走原生方法從數據庫獲取對象,而且將結果保存到緩存中
- 若是當前方法在配置的方法中,而且爲刪除緩存方法,則組裝全部須要刪除的key,並從緩存中刪除remove掉對應的記錄
- 流程結束。
#####關鍵步驟代碼 項目的幾個關鍵步驟,用到了spring處理aop的一些接口和類,這裏拿出來作下說明,app
- 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; } }
- 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; } }
- com.nbtv.proxycache.aop.advice.CacheManagerRoundAdvice,真正的代理方法,執行對緩存的操做工做,結構以下 public class CacheManagerRoundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //實現了MethodInterceptor的方法,此方法即目標bean的代理處理方法 //處理緩存方法 } }
#####項目啓動時spring所作的操做以下:框架
- 系統啓動構造ClassPathXmlApplicationContext對象
- 調用org.springframework.context.support. ClassPathXmlApplicationContext 的ClassPathXmlApplicationContext執行refresh()操做;(refresh方法對beanFactory進程一系列操做,並對工廠bean、監聽bean、特殊bean等進行初始化)。
- Refresh()方法中執行registerBeanPostProcessors()方法
- 此方法若是當前bean是所配置的緩存加載bean CacheManage,則執行其init方法,讀取配置文件。詳見其繼承類LocalConfigCacheManager. loadConfig()方法
- Refresh()方法中執行finishBeanFactoryInitialization(beanFactory)方法對其餘普通bean進行處理
- finishBeanFactoryInitialization方法調用org.springframework.beans.factory.support. DefaultListableBeanFactory的preInstantiateSingletons(),循環全部bean的名稱,調用getBean(beanName)方法,對bean初始化。
- 調用org.springframework.beans.factory.support. AbstractBeanFactory的doGetBean方法,此方法先作一些校驗操做,而後調用getSingleton()方法,構造單例對象
- 調用org.springframework.beans.factory.support. AbstractAutowireCapableBeanFactory的createBean方法此方法先嚐試構造前置代理,若是失敗,則調用doCreateBean方法建立bean
- 以後調用到initializeBean方法,先初始化bean,
- 而後調用applyBeanPostProcessorsAfterInitialization()方法構造bean的處理器
- applyBeanPostProcessorsAfterInitialization調用其父類的getBeanPostProcessors()方法,獲取到全部繼承了AbstractAutoProxyCreator類的類,遍歷全部類並執行其postProcessAfterInitialization方法
- 執行com.nbtv.proxycache.aop.handle. CacheManagerHandle的getAdvicesAndAdvisorsForBean方法,判斷當前的bean是否在緩存代理配置文件中配置了須要代理,若是須要,則建立一個引介切面,並返回該引介切面
- 引介切面做爲參數,調用AbstractAutoProxyCreator. wrapIfNecessary中調用createProxy方法,構建代理對象,並返回。
- 代理對象爲jdkDynamicAopProxy,
- Refresh()方法中執行finishRefresh()方法進行收尾操做
- finishRefresh()方法調用publishEvent,發佈ContextRefreshedEvent事件,並廣播該事件
- CacheManager. onApplicationEvent監聽到ContextRefreshedEvent事件,對配置的cacheBean分別進行填充和靜態校驗,並初始化緩存,詳細實現請看代碼
- 至此,已經初始化了全部的bean,而且根據配置構好了代理的對象
#####系統運行時流程以下(以service調用Dao,dao被配置了緩存代理爲例):
- Service中獲取到的memberDao對象,此對象其實是原Dao的代理對象$proxy
- $proxy執行代理的前置方法,對緩存進行操做(組裝key,而且添加或者刪除緩存,具體執行流程請查看CacheManagerRoundAdvice. Invoke()方法)
- 若是是刪除緩存方法,則會繼續調用invocation.proceed(),嘗試執行接下來的代理方法或者原生方法
- 若是有其餘代理,則找到了下一個代理並執行相應方法,
- 調用原生方法
####項目新增和修改過的功能:
- 緩存bean和刪除緩存方法都加上了<prefix></prefix>用來防止不一樣的集羣項目使用到相同的bean
- methodConfig中增長<cache></cache>配置,容許不一樣的緩存方法是用不一樣的緩存bean,
- 參數類型不參與緩存key的組成,由於參數的值已經能夠知足key的惟一性,加上類型會使key增加
- 增長<parametersIndexs></parametersIndexs>標籤,爲參與緩存key的參數順序,好比有三個參數,可是隻有第一個和第二個參與key,而且第二個參數是個對象,對象中的userName參與key組裝,則配置爲1,2#userName
- 對刪除緩存方法的引用方法去除bean的正確性校驗,因爲集羣,可能引用方法並不在本項目中,校驗確定是失敗的。
- 緩存修改支持使用不一樣的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>
後記:此項目雖然是一個比較簡單的功能,可是項目的做者考慮的很是周全,功能比較齊全,代碼很是規範,思路很是清晰,而且大量用到了設計模式,能夠看出做者的功底很是好。讀此項目,受益不淺,向做者致敬。