鍍金池/ 教程/ Java/ 緩存相關(guān)代碼的演變
分布式鎖的簡(jiǎn)單實(shí)現(xiàn)
關(guān)于框架體系與戰(zhàn)術(shù)的思考
開源與中小型軟件公司的未來趨勢(shì)
生態(tài)圈的建立
用200行的DBF解析器來展示良好架構(gòu)設(shè)計(jì)
緣起
業(yè)務(wù)流程引擎設(shè)計(jì)
軟件開發(fā)雜談
高屋建瓴,理念先行
借船下海還是造船下海
Web界面快速開發(fā)實(shí)踐
教計(jì)算機(jī)程序解數(shù)學(xué)題
量身定制規(guī)則引擎,適應(yīng)多變業(yè)務(wù)場(chǎng)景
緩存相關(guān)代碼的演變
理想的開源框架與設(shè)計(jì)原則
框架2.0的設(shè)計(jì)梳理
與屈原對(duì)話及開源精神

緩存相關(guān)代碼的演變

問題引入

上次我參與某個(gè)大型項(xiàng)目的優(yōu)化工作,由于系統(tǒng)要求有比較高的TPS,因此就免不了要使用緩沖。

該項(xiàng)目中用的緩沖比較多,有MemCache,有Redis,有的還需要提供二級(jí)緩沖,也就是說應(yīng)用服務(wù)器這層也可以設(shè)置一些緩沖。

當(dāng)然去看相關(guān)實(shí)現(xiàn)代代碼的時(shí)候,大致是下面的樣子。

public void saveSomeObject(SomeObject someObject){  

    MemCacheUtil.put("SomeObject",someObject.getId(),someObject);  

    //下面是真實(shí)保存對(duì)象的代碼  

}  

public SomeObject getSomeObject(String id){  

    SomeObject someObject = MemCacheUtil.get("SomeObject",id);  

    if(someObject!=null){  

         someObject=//真實(shí)的獲取對(duì)象  

         MemCacheUtil.put("SomeObject",someObject.getId(),someObject);  

    }  

    return someObject;  

}  

很明顯與緩沖相關(guān)的代碼全部是耦合到原來的業(yè)務(wù)代碼當(dāng)中去的。

后來由于MemCache表現(xiàn)不夠穩(wěn)定,而且MemCache的功能,也可以由Redis完全進(jìn)行實(shí)現(xiàn),于是就決定從系統(tǒng)中取消MemCache,換成Redis的實(shí)現(xiàn)方案,于是就改成如下的樣子:

public void saveSomeObject(SomeObject someObject){  

    RedisUtil.put("SomeObject",someObject.getId(),someObject);  

    //下面是真實(shí)保存對(duì)象的代碼  

}  

public SomeObject getSomeObject(String id){  

    SomeObject someObject = RedisUtil.get("SomeObject",id);  

    if(someObject!=null){  

         someObject=//真實(shí)的獲取對(duì)象 <span></span>RedisUtil.put("SomeObject",someObject.getId(),someObject);  

    }  

    return someObject;  

}  

這一通改下來,開發(fā)人員已經(jīng)暈頭暈?zāi)X的了,后來感覺性能還是不夠高,這個(gè)時(shí)候,要把一些數(shù)據(jù)增加二級(jí)緩沖,也就是說,本地緩沖有就取本地,本地沒有就取遠(yuǎn)程緩沖

于是,上面的代碼又是一通改,變成下面這個(gè)樣子:

public void saveSomeObject(SomeObject someObject){  

    LocalCacheUtil.put("SomeObject",someObject.getId(),someObject);  

    RedisUtil.put("SomeObject",someObject.getId(),someObject);  

    //下面是真實(shí)保存對(duì)象的代碼  

}  

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=//真實(shí)的獲取對(duì)象   

         RedisUtil.put("SomeObject",someObject.getId(),someObject);  

    }  

    return someObject;  

}  

但是這個(gè)時(shí)候就出現(xiàn)一個(gè)問題:

由于在某一時(shí)刻修改值的只能是某一臺(tái)計(jì)算機(jī),這個(gè)時(shí)候,其它的計(jì)算機(jī)的本地緩沖實(shí)際上與遠(yuǎn)程及數(shù)據(jù)庫中的數(shù)據(jù)會(huì)不一致,這個(gè)時(shí)候,可以有兩種辦法實(shí)現(xiàn),一種是利用Redis的請(qǐng)閱發(fā)布機(jī)制進(jìn)行數(shù)據(jù)同步,這種方式,會(huì)保證數(shù)據(jù)能夠被及時(shí)同步。

另外一種方法就是設(shè)置本地緩沖的有效時(shí)間比較短,這樣,允許在比較短的時(shí)間段內(nèi)出現(xiàn)數(shù)據(jù)不一致的情況。

不管怎么樣,功能是實(shí)現(xiàn)了,程序員小伙伴這個(gè)時(shí)候已經(jīng)改得眼睛發(fā)黑,手指發(fā)麻,幾乎接近崩潰了。

很明顯這種實(shí)現(xiàn)方式是不好的,于是項(xiàng)目組又提出了改進(jìn)意見,能否采用注解方式進(jìn)行標(biāo)注,讓程序員只要聲明就可以?Good idea,于是,又變成了下面的樣子:

@Cache(type="SomeObject",parameter="someObject",key="${someObject.id}")  

public void saveSomeObject(SomeObject someObject){  

    //下面是真實(shí)保存對(duì)象的代碼  

}  

@Cache("SomeObject",key="${id}")  

public SomeObject getSomeObject(String id){  

    SomeObject someObject=//真實(shí)的獲取  

    return someObject;  

}  

這個(gè)時(shí)候,程序員們的代碼已經(jīng)非常清爽了,里面不再有與緩沖相關(guān)的部分內(nèi)容,但是引入一個(gè)新的問題,就是處理注解的代碼怎么寫?需要引入容器,比如:Spring,這些Bean必須被容器所托管,如果直接new一個(gè)實(shí)例,就沒有辦法用緩沖了。還有一個(gè)問題是:程序員的工作量雖然有所節(jié)省,但是還是要對(duì)程序代碼有侵入性,需要引入這些注解,如果要增加超越現(xiàn)有注解的功能,還是需要重新寫過這些類,引入其它的注解,修改現(xiàn)有的注解。

所以,上面是個(gè)可以接受的方案,但明顯還不是很好的方案。

假如有一個(gè)程序員火大了,他發(fā)出下面的抱怨:“我只管做我的業(yè)務(wù),放不放緩沖和我有1毛錢關(guān)系么?總因?yàn)榫彌_的事情讓我改來改去,程序改得亂七八糟不說,我的時(shí)間,我的工作進(jìn)度都影響了誰來管?以后和緩沖相關(guān)的事情別他媽的來煩我!”,作為架構(gòu)師的你,你怎么看?最起碼,我覺得他是說得非常有道理的。我們?cè)俜颠^頭來看看最原始的代碼:

public void saveSomeObject(SomeObject someObject){  

    //下面是真實(shí)保存對(duì)象的代碼  

}  

public SomeObject getSomeObject(String id){  

    SomeObject someObject=//真實(shí)的獲取  

    return someObject;  

}  

這里是干干凈凈的業(yè)務(wù)代碼,和緩沖沒有一點(diǎn)關(guān)系。后來由于性能方面的要求,需要做緩沖,OK,這一點(diǎn)是事實(shí),但是用什么緩沖或怎么緩沖,與程序員確實(shí)是沒有什么關(guān)系的,因此,是不是可以不讓程序員參與,就可以優(yōu)雅的做到添加緩沖功能呢?答案當(dāng)然是肯定的。

需求整理

代碼當(dāng)中,不要體現(xiàn)與緩沖相關(guān)的內(nèi)容,也就是說做不做緩沖及怎么做緩沖不要影響到業(yè)務(wù)代碼 不管是從容器中取實(shí)例還是new實(shí)例,都可以同樣的起作用,也就是說可以不必依賴具體的容器

解決思路:

放不放緩沖、怎么放緩沖、緩沖有效時(shí)間等等,這些內(nèi)容是在運(yùn)行期發(fā)現(xiàn)存在性能瓶頸,然后提交給程序員來進(jìn)行優(yōu)化的。為此,我們?cè)O(shè)計(jì)了一個(gè)配置來描述這些緩沖相關(guān)的聲明。

當(dāng)然,這個(gè)配置文件的結(jié)構(gòu),可以根據(jù)自己所采用的緩沖框架來進(jìn)行相應(yīng)的定義。

比如:

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

我們?cè)趯?shí)際應(yīng)用當(dāng)中,配置比上面的示例更完善,那現(xiàn)在我先講一下上面的兩段配置的含義。

在UserDao的saveUser的時(shí)候,會(huì)同步的把User數(shù)據(jù)放到Redis中進(jìn)行緩沖,緩沖時(shí)間為1秒,存放的緩沖數(shù)據(jù)的類型為user,鍵值為${user.id},也就是要保存的用戶的主健。實(shí)際進(jìn)入到Redis的時(shí)候,在Redis中的健值是由上面類型與key的共同組成的。

在調(diào)用UserDao的getUser的時(shí)候,會(huì)先從緩沖中獲取類型為user,鍵值為${id}的數(shù)據(jù),如果緩沖中在,則取出并返回,如果緩沖中沒有,則從原有業(yè)務(wù)代碼中取出值并放入緩沖,然后返回此對(duì)象。

哇,這個(gè)時(shí)候,就非常爽了,只要通過聲明就可以做到對(duì)緩沖的處理了,但是一個(gè)問題就出來了,如何實(shí)現(xiàn)上面的需求呢?

通過配置文件外置,確實(shí)做到了對(duì)業(yè)務(wù)代碼的0侵入,但是如何為原有業(yè)務(wù)增加緩沖相當(dāng)?shù)臉I(yè)務(wù)邏輯呢?由于需求2要求可以new,也可以從容器中獲取對(duì)象實(shí)例,因此利用容器AOP解決的跑就被封死了,因此,就得引入字節(jié)碼的方式來進(jìn)行解決。

具體實(shí)現(xiàn)

寫一個(gè)基于Maven的緩沖代碼處理插件,在編譯后增加此處理插件,根據(jù)配置文件對(duì)原有代碼進(jìn)行掃描并修改其字節(jié)碼以添加緩沖相關(guān)處理邏輯。

現(xiàn)在只要使用Maven進(jìn)行compile或install就可以自動(dòng)添加緩沖相關(guān)的邏輯到class文件中了。

至此,我們已經(jīng)分析了緩沖代碼直接耦合到代碼中,并分析了其中的缺點(diǎn),最終演化了注解方式,外置配置方式,并簡(jiǎn)要介紹了實(shí)現(xiàn)方法。

具體實(shí)現(xiàn),采用的技術(shù)就比較多了,有Maven插件、有Asm、有模板引擎還有Tiny框架的一些基礎(chǔ)工程,如:VFS,F(xiàn)ileResolver等等。

如果采用Tiny框架,可以直接拿來用,如果不用Tiny框架,可以參照上面的思路做自己的實(shí)現(xiàn)。