最近在看Mybatis的源碼,剛好看到緩存這一塊,Mybatis提供了一級(jí)緩存和二級(jí)緩存。一級(jí)緩存相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,功能比較齊全的是二級(jí)緩存,基本上滿足了一個(gè)緩存該有的功能。當(dāng)然如果拿來(lái)和專門的緩存框架如ehcache來(lái)對(duì)比可能稍有差距。
本文千鋒武漢Java小編將來(lái)整理一下實(shí)現(xiàn)一個(gè)本地緩存都應(yīng)該需要考慮哪些東西?一起往下看吧。
一、考慮點(diǎn)
考慮點(diǎn)主要在數(shù)據(jù)用何種方式存儲(chǔ),能存儲(chǔ)多少數(shù)據(jù),多余的數(shù)據(jù)如何處理等幾個(gè)點(diǎn),下面小編來(lái)詳細(xì)的介紹每個(gè)考慮點(diǎn),以及該如何去實(shí)現(xiàn)。
1.數(shù)據(jù)結(jié)構(gòu)
首要考慮的就是數(shù)據(jù)該如何存儲(chǔ),用什么數(shù)據(jù)結(jié)構(gòu)存儲(chǔ),最簡(jiǎn)單的就直接用Map來(lái)存儲(chǔ)數(shù)據(jù)?;蛘邚?fù)雜的如redis一樣提供了多種數(shù)據(jù)類型哈希,列表,集合,有序集合等,底層使用了雙端鏈表,壓縮列表,集合,跳躍表等數(shù)據(jù)結(jié)構(gòu)。
2.對(duì)象上限
因?yàn)槭潜镜鼐彺妫瑑?nèi)存有上限,所以一般都會(huì)指定緩存對(duì)象的數(shù)量比如1024,當(dāng)達(dá)到某個(gè)上限后需要有某種策略去刪除多余的數(shù)據(jù)。
3.清除策略
上面說(shuō)到當(dāng)達(dá)到對(duì)象上限之后需要有清除策略,常見(jiàn)的比如有LRU(最近最少使用)、FIFO(先進(jìn)先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)等策略。
4.過(guò)期時(shí)間
除了使用清除策略,一般本地緩存也會(huì)有一個(gè)過(guò)期時(shí)間設(shè)置,比如redis可以給每個(gè)key設(shè)置一個(gè)過(guò)期時(shí)間,這樣當(dāng)達(dá)到過(guò)期時(shí)間之后直接刪除,采用清除策略+過(guò)期時(shí)間雙重保證。
5.線程安全
像redis是直接使用單線程處理,所以就不存在線程安全問(wèn)題。而我們現(xiàn)在提供的本地緩存往往是可以多個(gè)線程同時(shí)訪問(wèn)的,所以線程安全是不容忽視的問(wèn)題。并且線程安全問(wèn)題是不應(yīng)該拋給使用者去保證。
6.簡(jiǎn)明的接口
提供一個(gè)傻瓜式的對(duì)外接口是很有必要的,對(duì)使用者來(lái)說(shuō)使用此緩存不是一種負(fù)擔(dān)而是一種享受。提供常用的get,put,remove,clear,getSize方法即可。
7.是否持久化
這個(gè)其實(shí)不是必須的,是否需要將緩存數(shù)據(jù)持久化看需求。本地緩存如ehcache是支持持久化的,而guava是沒(méi)有持久化功能的。分布式緩存如redis是有持久化功能的,memcached是沒(méi)有持久化功能的。
8.阻塞機(jī)制
在看Mybatis源碼的時(shí)候,二級(jí)緩存提供了一個(gè)blocking標(biāo)識(shí),表示當(dāng)在緩存中找不到元素時(shí),它設(shè)置對(duì)緩存鍵的鎖定。這樣其他線程將等待此元素被填充,而不是命中數(shù)據(jù)庫(kù)。其實(shí)我們使用緩存的目的就是因?yàn)楸痪彺娴臄?shù)據(jù)生成比較費(fèi)時(shí),比如調(diào)用對(duì)外的接口,查詢數(shù)據(jù)庫(kù),計(jì)算量很大的結(jié)果等等。這時(shí)候如果多個(gè)線程同時(shí)調(diào)用get方法獲取的結(jié)果都為null,每個(gè)線程都去執(zhí)行一遍費(fèi)時(shí)的計(jì)算,其實(shí)也是對(duì)資源的浪費(fèi)。最好的辦法是只有一個(gè)線程去執(zhí)行,其他線程等待,計(jì)算一次就夠了。但是此功能基本上都交給使用者來(lái)處理,很少有本地緩存有這種功能。
二、如何實(shí)現(xiàn)
以上大致介紹了實(shí)現(xiàn)一個(gè)本地緩存我們都有哪些需要考慮的地方,當(dāng)然可能還有其他沒(méi)有考慮到的點(diǎn)。下面繼續(xù)看看關(guān)于每個(gè)點(diǎn)都應(yīng)該如何去實(shí)現(xiàn),重點(diǎn)介紹一下思路。
1.數(shù)據(jù)結(jié)構(gòu)
本地緩存最常見(jiàn)的是直接使用Map來(lái)存儲(chǔ),比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二級(jí)緩存使用HashMap來(lái)存儲(chǔ):
Map
當(dāng)然除了使用Map來(lái)存儲(chǔ),可能還使用其他數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ),比如redis使用了雙端鏈表,壓縮列表,整數(shù)集合,跳躍表和字典。當(dāng)然這主要是因?yàn)閞edis對(duì)外提供的接口很豐富除了哈希還有列表,集合,有序集合等功能。
2.對(duì)象上限
本地緩存常見(jiàn)的一個(gè)屬性,一般緩存都會(huì)有一個(gè)默認(rèn)值比如1024,在用戶沒(méi)有指定的情況下默認(rèn)指定。當(dāng)緩存的數(shù)據(jù)達(dá)到指定最大值時(shí),需要有相關(guān)策略從緩存中清除多余的數(shù)據(jù)這就涉及到下面要介紹的清除策略。
3.清除策略
配合對(duì)象上限之后使用,場(chǎng)景的清除策略如:LRU(最近最少使用)、FIFO(先進(jìn)先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)。
LRU:Least Recently
Used的縮寫最近最少使用,移除最長(zhǎng)時(shí)間不被使用的對(duì)象。常見(jiàn)的使用LinkedHashMap來(lái)實(shí)現(xiàn),也是很多本地緩存默認(rèn)使用的策略。
FIFO:先進(jìn)先出,按對(duì)象進(jìn)入緩存的順序來(lái)移除它們。常見(jiàn)使用隊(duì)列Queue來(lái)實(shí)現(xiàn)。
LFU:Least Frequently
Used的縮寫大概也是最近最少使用的意思,和LRU有點(diǎn)像。區(qū)別點(diǎn)在LRU的淘汰規(guī)則是基于訪問(wèn)時(shí)間,而LFU是基于訪問(wèn)次數(shù)的。可以通過(guò)HashMap并且記錄訪問(wèn)次數(shù)來(lái)實(shí)現(xiàn)。
SOFT:軟引用基于垃圾回收器狀態(tài)和軟引用規(guī)則移除對(duì)象。常見(jiàn)使用SoftReference來(lái)實(shí)現(xiàn)。
WEAK:弱引用更積極地基于垃圾收集器狀態(tài)和弱引用規(guī)則移除對(duì)象。常見(jiàn)使用WeakReference來(lái)實(shí)現(xiàn)。
4.過(guò)期時(shí)間
設(shè)置過(guò)期時(shí)間,讓緩存數(shù)據(jù)在指定時(shí)間過(guò)后自動(dòng)刪除。常見(jiàn)的過(guò)期數(shù)據(jù)刪除策略有兩種方式:被動(dòng)刪除和主動(dòng)刪除。
被動(dòng)刪除:每次進(jìn)行g(shù)et/put操作的時(shí)候都會(huì)檢查一下當(dāng)前key是否已經(jīng)過(guò)期,如果過(guò)期則刪除,類似如下代碼:
?
主動(dòng)刪除:專門有一個(gè)job在后臺(tái)定期去檢查數(shù)據(jù)是否過(guò)期,如果過(guò)期則刪除,這其實(shí)可以有效的處理冷數(shù)據(jù)。
5.線程安全
盡量用線程安全的類去存儲(chǔ)數(shù)據(jù),比如使用ConcurrentHashMap代替HashMap?;蛘咛峁┫鄳?yīng)的同步處理類,比如Mybatis提供了SynchronizedCache:
?
6.簡(jiǎn)明的接口
提供常用的get,put,remove,clear,getSize方法即可,比如Mybatis的Cache接口:
?
再來(lái)看看guava提供的Cache接口,相對(duì)來(lái)說(shuō)也是比較簡(jiǎn)潔的:
?
7.是否持久化
持久化的好處是重啟之后可以再次加載文件中的數(shù)據(jù),這樣就起到類似熱加載的功效。比如ehcache提供了是否持久化磁盤緩存的功能,將緩存數(shù)據(jù)存放在一個(gè).data文件中。
?
redis更是將持久化功能發(fā)揮到極致,慢慢的有點(diǎn)像數(shù)據(jù)庫(kù)了。提供了AOF和RDB兩種持久化方式。當(dāng)然很多情況下可以配合使用兩種方式。
8.阻塞機(jī)制
除了在Mybatis中看到了BlockingCache來(lái)實(shí)現(xiàn)此功能,之前在看<>的時(shí)候其中有實(shí)現(xiàn)一個(gè)很完美的緩存,大致代碼如下:
?
compute是一個(gè)計(jì)算很費(fèi)時(shí)的方法,所以這里把計(jì)算的結(jié)果緩存起來(lái),但是有個(gè)問(wèn)題就是如果兩個(gè)線程同時(shí)進(jìn)入此方法中怎么保證只計(jì)算一次,這里最核心的地方在于使用了ConcurrentHashMap的putIfAbsent方法,同時(shí)只會(huì)寫入一個(gè)FutureTask。
總結(jié)
本文大致介紹了要設(shè)計(jì)一個(gè)本地緩存都需要考慮哪些點(diǎn):數(shù)據(jù)結(jié)構(gòu),對(duì)象上限,清除策略,過(guò)期時(shí)間,線程安全,阻塞機(jī)制,實(shí)用的接口,是否持久化。當(dāng)然肯定有其他考慮點(diǎn),歡迎補(bǔ)充。
想了解更多Java方面的知識(shí),你可以關(guān)注“千鋒武漢”公眾號(hào),定期發(fā)布技術(shù)熱點(diǎn)和行業(yè)趨勢(shì)分析,助力你快速入職。
2021-10-22 千鋒武漢發(fā)布了 《《我的世界》千鋒1024程序員節(jié)品牌片重磅發(fā)布》的文章
2021-10-22 千鋒武漢發(fā)布了 《千鋒1024程序員節(jié)重磅激勵(lì),多重豪禮強(qiáng)力助學(xué)》的文章
2021-10-22 千鋒武漢發(fā)布了 《千鋒聯(lián)動(dòng)全國(guó)百所院校 開(kāi)展1024程序員節(jié)狂“享”活動(dòng)!》的文章
2021-10-20 千鋒武漢發(fā)布了 《千鋒為中國(guó)航發(fā)商發(fā)提供Python課程培訓(xùn),助力商發(fā)公司高效決策》的文章
2021-10-15 千鋒武漢發(fā)布了 《千鋒教育1024程序員狂歡節(jié)即將火爆來(lái)襲 “厚禮”一觸即發(fā)》的文章