四種緩存的避坑總結(jié)
背景
分布式、緩存、異步和多線程被稱為互聯(lián)網(wǎng)開發(fā)的四大法寶。今天我總結(jié)一下項目開發(fā)中常接觸的四種緩存實際項目中遇到過的問題。
JVM 堆內(nèi)緩存
JVM 堆內(nèi)緩存因為可以避免 Memcached、Redis 等集中式緩存網(wǎng)絡(luò)通信故障問題,目前還在項目中廣泛使用。
堆內(nèi)緩存需要注意 GC 的問題。假如我們的設(shè)計是定時的從遠程來拉取數(shù)據(jù)更新本地緩存。一定要注意兩點:第一不要全量拉取覆蓋,第二不要把一個大對象整體替換為新對象。
先說全量拉取覆蓋。全量拉取會有很大的網(wǎng)絡(luò)開銷,會造成網(wǎng)絡(luò)流量尖刺。有人說沒事,我們帶寬很足,內(nèi)網(wǎng)訪問,不怕不怕。但是穩(wěn)定性需要修煉的一項是削峰填谷。讓系統(tǒng)在平穩(wěn)的環(huán)境中運行。不然,在拉取大緩存新數(shù)據(jù)的數(shù)據(jù)突然來了個突發(fā)流量?根據(jù)墨菲定律,凡是有幾率會發(fā)生的事情就一定會發(fā)生。編程需謹慎。
再說大對象整體替換的問題,這會造成 GC 問題。偽代碼如下:
List<POJO> oldList = initList();
public void refresh() {
List<POJO> newList = dataFromNetworkService.getAll();
oldList = new List();
for(POJO pojo : newList) {
oldList.add(pojo);
}
}
如果從網(wǎng)上拉取的數(shù)據(jù)和在緩存里存儲的數(shù)據(jù),對象類型沒有發(fā)生改變。引起的轉(zhuǎn)換開銷還稍微小點。因為比如對象 POJO 存在一個列表里。這個列表雖然很大,但是里面存的都是對象的引用。實際的 POJO 并沒有發(fā)生變化。上面?zhèn)未a雖然新建一個 List 對象,遍歷添加新對象比直接 oldList=newList 要傻些。但是遍歷過程實際上 POJO 對象沒有發(fā)生改變。所以這里影響 GC 的只是 oldList 這個對象(不包括從網(wǎng)絡(luò)上拉取回來數(shù)據(jù)的過程)。
但是如果代碼這樣寫:
List<POJO2> oldList = initList();
public void refresh() {
List<POJO1> newList = dataFromNetworkService.getAll();
oldList = new List();
for(POJO2 pojo : newList) {
oldList.add(Beanutils.copy(new POJO2(), pojo));
}
}
遍歷過程將會將原來的 POJO1 全部新建一遍,這些對象一般情況下全部先進入堆內(nèi)存的新生代,再經(jīng)過數(shù)次 Young GC 后進入老年代。會造成GC頻繁。
我所做過的項目,一般認為一天一到兩次 Full GC 為合理值。這樣,如果比如預(yù)先知道某個時間點有大促,可通過提前觸發(fā) GC 等方式避免高峰期爆發(fā) Full GC。Young GC 至少是 5 分鐘一次,甚至更久觸發(fā)認為是正常。這樣可以通過控制避過秒殺等場景。
JVM 堆外緩存
堆外緩存的內(nèi)存回收原理使用的是 Java 的虛引用 。這個設(shè)計可以避免 JVM 的 GC 問題,但是處理不好可能會造成更嚴重的后果:整個機器內(nèi)存被打滿,機器可能會掛掉。 其實掛掉一臺在一般企業(yè)的生產(chǎn)環(huán)境還好,因為一般都會有容災(zāi)的冗余機器。 但是更常見的一種情況是機器忙于 swap 內(nèi)存交換,機器活著但是響應(yīng)很慢。 屬于半死不活。 這個問題我沒在線上遇到過,但是我同事之前在超級大廠的時候遇到過。
有的同學(xué)說那我嚴格算好內(nèi)存,做好監(jiān)控。這里面要就要依賴人為的因素來做緊急處理。而人是穩(wěn)定性中最不可靠的。因為問題通常不發(fā)生在人清醒、手里事情很少的時候。而是一種雪上加霜的存在。比如大促時,流量上來了,線程數(shù)會增多,每個線程都會申請線程棧資源,系統(tǒng)處理 IO,這時候系統(tǒng)會申請更多的 buffers/cached 內(nèi)存。
Linux 的 buffers/cached
Linux 系統(tǒng)上運行一下 top 命令或者 free 命令,都能夠看到 buffers 和 cached 相關(guān)的數(shù)據(jù)。需要注意的是通常我們看到的監(jiān)控數(shù)據(jù)空閑內(nèi)存百分比,并非是下面顯示的 free/total,而是 (free+buffers+cached)/total。
buffers 在 Linux 系統(tǒng)中通常被作為與塊存儲的 IO 緩存使用。所謂塊存儲可簡單理解為將數(shù)據(jù)直接寫到裸磁盤。而 cached 則一般會用于文件系統(tǒng)的 IO 緩存。比如 page cache 這種內(nèi)存換頁功能。
聽不明白也沒關(guān)系,因為事實上它們兩個經(jīng)常配合使用。比如與磁盤交換數(shù)據(jù)、進行網(wǎng)絡(luò)通信時都會用。buffers 和 cached 是實實在在被操作系統(tǒng)的系統(tǒng)進程在使用的,但是如果用戶進程需要可以很快釋放。所以通常會將它算到剩余可用內(nèi)存里。
但是這個也要注意了。比如在 IO 密集型的系統(tǒng),如果 buffers/cached 被大幅占用,會降低 IO 速度,進而降低系統(tǒng)吞吐。甚至有可能一個請求幾秒才能到達應(yīng)用程序,造成請求超時。
集中式緩存
Redis 緩存其實也有本機代理,可以緩存一些活躍的數(shù)據(jù)在本機上,本機可以在取 不 到數(shù)據(jù)時不需要跨網(wǎng)絡(luò)通信。但是因為 Redis 本質(zhì)是 key-value 的結(jié)構(gòu)。如果需要根據(jù)通配符取數(shù)據(jù)全量,如果網(wǎng)絡(luò)出現(xiàn)故障,可能會影響數(shù)據(jù)的完整性。
但是 Redis 緩存最讓人擔(dān)心的是不規(guī)范的使用方法。比如存一個很大的 value。具體這個對網(wǎng)絡(luò)和存儲造成的問題就不詳細說了。可以想象下馬桶堵了的情景。
總結(jié)
貝爾實驗室的面向?qū)ο缶幊虒<?Tom Cargill 說:
最初 90% 的開發(fā)工作將會用去你最初 90% 的開發(fā)時間,剩下的 10% 的開發(fā)量將會用去你另外一個 90% 的開發(fā)時間。
我理解剩下 10% 占用了 90% 的時間是由于超出了原有知識貯備,需要臨時抱佛腳,甚至需要拿著錘子找釘子造成的。所以或者也可以這樣做:
每周持續(xù)投入 5% 的學(xué)習(xí)時間,10% 的思考時間,再用 100% 的時間去完成 100% 的開發(fā)。