緩存相關代碼的演變
問題引入
上次我參與某個大型項目的優化工作,由于系統要求有比較高的TPS,因此就免不了要使用緩沖。
該項目中用的緩沖比較多,有MemCache,有Redis,有的還需要提供二級緩沖,也就是說應用服務器這層也可以設置一些緩沖。
當然去看相關實現代代碼的時候,大致是下面的樣子。
public void saveSomeObject(SomeObject someObject){ MemCacheUtil.put("SomeObject",someObject.getId(),someObject); //下面是真實保存對象的代碼 } public SomeObject getSomeObject(String id){ SomeObject someObject = MemCacheUtil.get("SomeObject",id); if(someObject!=null){ someObject=//真實的獲取對象 MemCacheUtil.put("SomeObject",someObject.getId(),someObject); } return someObject; }
很明顯與緩沖相關的代碼全部是耦合到原來的業務代碼當中去的。
后來由于MemCache表現不夠穩定,而且MemCache的功能,也可以由Redis完全進行實現,于是就決定從系統中取消MemCache,換成Redis的實現方案,于是就改成如下的樣子:
public void saveSomeObject(SomeObject someObject){ RedisUtil.put("SomeObject",someObject.getId(),someObject); //下面是真實保存對象的代碼 } public SomeObject getSomeObject(String id){ SomeObject someObject = RedisUtil.get("SomeObject",id); if(someObject!=null){ someObject=//真實的獲取對象 RedisUtil.put("SomeObject",someObject.getId(),someObject); } return someObject; }這一通改下來,開發人員已經暈頭暈腦的了,后來感覺性能還是不夠高,這個時候,要把一些數據增加二級緩沖,也就是說,本地緩沖有就取本地,本地沒有就取遠程緩沖
于是,上面的代碼又是一通改,變成下面這個樣子:
public void saveSomeObject(SomeObject someObject){ LocalCacheUtil.put("SomeObject",someObject.getId(),someObject); RedisUtil.put("SomeObject",someObject.getId(),someObject); //下面是真實保存對象的代碼 } public SomeObject getSomeObject(String id){ SomeObject someObject = LocalCacheUtil.get("SomeObject",id); if(someObject!=null){ return someObject; } someObject = RedisUtil.get("SomeObject",id); if(someObject!=null){ someObject=//真實的獲取對象 RedisUtil.put("SomeObject",someObject.getId(),someObject); } return someObject; }但是這個時候就出現一個問題:
由于在某一時刻修改值的只能是某一臺計算機,這個時候,其它的計算機的本地緩沖實際上與遠程及數據庫中的數據會不一致,這個時候,可以有兩種辦法實現,一種是利用Redis的請閱發布機制進行數據同步,這種方式,會保證數據能夠被及時同步。
另外一種方法就是設置本地緩沖的有效時間比較短,這樣,允許在比較短的時間段內出現數據不一致的情況。
不管怎么樣,功能是實現了,程序員小伙伴這個時候已經改得眼睛發黑,手指發麻,幾乎接近崩潰了。
很明顯這種實現方式是不好的,于是項目組又提出了改進意見,能否采用注解方式進行標注,讓程序員只要聲明就可以?Good idea,于是,又變成了下面的樣子:
@Cache(type="SomeObject",parameter="someObject",key="${someObject.id}") public void saveSomeObject(SomeObject someObject){ //下面是真實保存對象的代碼 } @Cache("SomeObject",key="${id}") public SomeObject getSomeObject(String id){ SomeObject someObject=//真實的獲取 return someObject; }
這個時候,程序員們的代碼已經非常清爽了,里面不再有與緩沖相關的部分內容,但是引入一個新的問題,就是處理注解的代碼怎么寫?需要引入容器,比如:Spring,這些Bean必須被容器所托管,如果直接new一個實例,就沒有辦法用緩沖了。還有一個問題是:程序員的工作量雖然有所節省,但是還是要對程序代碼有侵入性,需要引入這些注解,如果要增加超越現有注解的功能,還是需要重新寫過這些類,引入其它的注解,修改現有的注解。
所以,上面是個可以接受的方案,但明顯還不是很好的方案。
假如有一個程序員火大了,他發出下面的抱怨:“我只管做我的業務,放不放緩沖和我有1毛錢關系么?總因為緩沖的事情讓我改來改去,程序改得亂七八糟不說,我的時間,我的工作進度都影響了誰來管?以后和緩沖相關的事情別他媽的來煩我!”,作為架構師的你,你怎么看?最起碼,我覺得他是說得非常有道理的。我們再返過頭來看看最原始的代碼:
public void saveSomeObject(SomeObject someObject){ //下面是真實保存對象的代碼 } public SomeObject getSomeObject(String id){ SomeObject someObject=//真實的獲取 return someObject; }這里是干干凈凈的業務代碼,和緩沖沒有一點關系。后來由于性能方面的要求,需要做緩沖,OK,這一點是事實,但是用什么緩沖或怎么緩沖,與程序員確實是沒有什么關系的,因此,是不是可以不讓程序員參與,就可以優雅的做到添加緩沖功能呢?答案當然是肯定的。
需求整理
- 代碼當中,不要體現與緩沖相關的內容,也就是說做不做緩沖及怎么做緩沖不要影響到業務代碼
- 不管是從容器中取實例還是new實例,都可以同樣的起作用,也就是說可以不必依賴具體的容器
解決思路:
放不放緩沖、怎么放緩沖、緩沖有效時間等等,這些內容是在運行期發現存在性能瓶頸,然后提交給程序員來進行優化的。為此,我們設計了一個配置來描述這些緩沖相關的聲明。
當然,這個配置文件的結構,可以根據自己所采用的緩沖框架來進行相應的定義。
比如:
<redis-caches> <redis-cache type="org.tinygroup.redis.test.UserDao"> <redis-method method-name="saveUser"> <redis-expire value="1000"></redis-expire> <redis-string type="user" key="${user.id}" paramter-name="user"><redis-string> </redis-method> </redis-cache> <redis-cache type="org.tinygroup.redis.test.UserDao"> <redis-method method-name="getUser"> <redis-expire value="1000"></redis-expire> <redis-string type="user" key="${id}" ><redis-string> </redis-method> </redis-cache> </redis-caches>
我們在實際應用當中,配置比上面的示例更完善,那現在我先講一下上面的兩段配置的含義。
在UserDao的saveUser的時候,會同步的把User數據放到Redis中進行緩沖,緩沖時間為1秒,存放的緩沖數據的類型為user,鍵值為${user.id},也就是要保存的用戶的主健。實際進入到Redis的時候,在Redis中的健值是由上面類型與key的共同組成的。
在調用UserDao的getUser的時候,會先從緩沖中獲取類型為user,鍵值為${id}的數據,如果緩沖中在,則取出并返回,如果緩沖中沒有,則從原有業務代碼中取出值并放入緩沖,然后返回此對象。
哇,這個時候,就非常爽了,只要通過聲明就可以做到對緩沖的處理了,但是一個問題就出來了,如何實現上面的需求呢?
通過配置文件外置,確實做到了對業務代碼的0侵入,但是如何為原有業務增加緩沖相當的業務邏輯呢?由于需求2要求可以new,也可以從容器中獲取對象實例,因此利用容器AOP解決的跑就被封死了,因此,就得引入字節碼的方式來進行解決。
具體實現
寫一個基于Maven的緩沖代碼處理插件,在編譯后增加此處理插件,根據配置文件對原有代碼進行掃描并修改其字節碼以添加緩沖相關處理邏輯。
現在只要使用Maven進行compile或install就可以自動添加緩沖相關的邏輯到class文件中了。
至此,我們已經分析了緩沖代碼直接耦合到代碼中,并分析了其中的缺點,最終演化了注解方式,外置配置方式,并簡要介紹了實現方法。
具體實現,采用的技術就比較多了,有Maven插件、有Asm、有模板引擎還有Tiny框架的一些基礎工程,如:VFS,FileResolver等等。
如果采用Tiny框架,可以直接拿來用,如果不用Tiny框架,可以參照上面的思路做自己的實現。
來自:http://my.oschina.net/tinyframework/blog/322913