高并發(fā)系統(tǒng)三大利器之緩存

引言

隨著互聯(lián)網(wǎng)的高速發(fā)展,市面上也出現(xiàn)了越來越多的網(wǎng)站和app。我們判斷一個軟件是否好用,用戶體驗就是一個重要的衡量標準。比如說我們經(jīng)常用的微信,打開一個頁面要十幾秒,發(fā)個語音要幾分鐘對方才能收到。相信這樣的軟件大家肯定是都不愿意用的。軟件要做到用戶體驗好,響應(yīng)速度快,緩存就是必不可少的一個神器。緩存又分進程內(nèi)緩存和分布式緩存兩種:分布式緩存如redis、memcached等,還有本地(進程內(nèi))緩存如ehcache、GuavaCacheCaffeine等。

緩存特征

緩存作為一個數(shù)據(jù)數(shù)據(jù)模型對象,那么它有一些什么樣的特征呢?下面我們分別來介紹下這些特征。

命中率

  • 命中率=命中數(shù)/(命中數(shù)+沒有命中數(shù))當某個請求能夠通過訪問緩存而得到響應(yīng)時,稱為緩存命中。緩存命中率越高,緩存的利用率也就越高。

最大空間

  • 緩存中可以容納最大元素的數(shù)量。當緩存存放的數(shù)據(jù)超過最大空間時,就需要根據(jù)淘汰算法來淘汰部分數(shù)據(jù)存放新到達的數(shù)據(jù)。

淘汰算法

  • 緩存的存儲空間有限制,當緩存空間被用滿時,如何保證在穩(wěn)定服務(wù)的同時有效提升命中率?這就由緩存淘汰算法來處理,設(shè)計適合自身數(shù)據(jù)特征的淘汰算法能夠有效提升緩存命中率。常見的淘汰算法有:
FIFO(first in first out)
  • 先進先出。最先進入緩存的數(shù)據(jù)在緩存空間不夠的情況下(超出最大元素限制)會被優(yōu)先被清除掉,以騰出新的空間接受新的數(shù)據(jù)。策略算法主要比較緩存元素的創(chuàng)建時間。適用于保證高頻數(shù)據(jù)有效性場景,優(yōu)先保障最新數(shù)據(jù)可用。
LFU(less frequently used)
  • 最少使用,無論是否過期,根據(jù)元素的被使用次數(shù)判斷,清除使用次數(shù)較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數(shù))。適用于保證高頻數(shù)據(jù)有效性場景。
LRU(least recently used)
  • 最近最少使用,無論是否過期,根據(jù)元素最后一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。比較適用于熱點數(shù)據(jù)場景,優(yōu)先保證熱點數(shù)據(jù)的有效性。

進程緩存

為什么需要引入本地緩存,本地緩存的應(yīng)用場景有哪些?

本地緩存的話是我們的應(yīng)用和緩存都在同一個進程里面,獲取緩存數(shù)據(jù)的時候純內(nèi)存操作,沒有額外的網(wǎng)絡(luò)開銷,速度非???。它適用于緩存一些應(yīng)用中基本不會變化的數(shù)據(jù),比如(國家、省份、城市等)。

項目中一般如何適用、怎么樣加載、怎么樣更新?

進程緩存的話,一般可以在應(yīng)用啟動的時候,把需要的數(shù)據(jù)加載到系統(tǒng)中。更新緩存的話可以采取定時更新(實時性不高)。具體實現(xiàn)的話就是在應(yīng)用中起一個定時任務(wù)(ScheduledExecutorService、TimerTask等),讓它每隔多久去加載變更(數(shù)據(jù)變更之后可以修改數(shù)據(jù)庫最后修改的時間,每次查詢變更數(shù)據(jù)的時候都可以根據(jù)這個最后變更時間加上半小時大于當前時間的數(shù)據(jù))的數(shù)據(jù)重新到緩存里面來。如果覺得這個比較麻煩的話,還可以直接全部全量更新(就跟項目啟動加載數(shù)據(jù)一樣)。這種方式的話,對數(shù)據(jù)更新可能會有點延遲。可能這臺機器看到的是更新后的數(shù)據(jù),那臺機器看到的數(shù)據(jù)還是老的(機器發(fā)布時間可能不一樣)。所以這種方式比較適用于對數(shù)據(jù)實時性要求不高的數(shù)據(jù)。如果對實時性有要求的話可以通過廣播訂閱mq消息。如果有數(shù)據(jù)更新mq會把更新數(shù)據(jù)推送到每一臺機器,這種方式的話實時性會比前一種定時更新的方法會好。但是實現(xiàn)起來會比較復雜。

在這里插入圖片描述

本地緩存有哪些實現(xiàn)方式?

常見本地緩存有以下幾種實現(xiàn)方式:

圖片來源于https://juejin.im/post/6844903665845665805

從上述表格我們看出性能最佳的是Caffeine。關(guān)于這個本地緩存的話我還是強烈推薦的,里面提供了豐富的api,以及各種各樣的淘汰算法。如需了解更加詳細的話可以看下以前寫的這個篇文章《本地緩存性能之王Caffeine》。

本地緩存缺點

  • 本地緩存與業(yè)務(wù)系統(tǒng)耦合再一起,應(yīng)用之間無法直接共享緩存的內(nèi)容。需要每個應(yīng)用節(jié)點單獨的維護自己的緩存。每個節(jié)點都需要一份一樣的緩存,對服務(wù)器內(nèi)存造成一種浪費。本地緩存機器重啟、或者宕機都會丟失。

分布式緩存

  • 分布式緩存是與應(yīng)用分離的緩存組件或服務(wù),其最大的優(yōu)點是自身就是一個獨立的應(yīng)用,與本地應(yīng)用隔離,多個應(yīng)用可直接的共享緩存。常見的分布式緩存有redisMemCache等。

分布式緩存的應(yīng)用

在高并發(fā)的環(huán)境下,比如春節(jié)搶票大戰(zhàn),一到放票的時間節(jié)點,分分鐘大量用戶以及黃牛的各種搶票軟件流量進入12306,這時候如果每個用戶的訪問都去數(shù)據(jù)庫實時查詢票的庫存,大量讀的請求涌入到數(shù)據(jù)庫,瞬間Db就會被打爆,cpu直接上升100%,服務(wù)馬上就要宕機或者假死。即使進行了分庫分表也是無法避免的。為了減輕db的壓力以及提高系統(tǒng)的響應(yīng)速度。一般都會在數(shù)據(jù)庫前面加上一層緩存,甚至可能還會有多級緩存。

緩存常見問題

緩存雪崩

指大量緩存同一時間段集體失效,或者緩存整體不能提供服務(wù),導致大量的請求全部到達數(shù)據(jù)庫
對數(shù)據(jù)CPU和內(nèi)存造成巨大壓力,嚴重的會造成數(shù)據(jù)庫宕機。因此而形成的一系列連鎖反應(yīng)造成整個系統(tǒng)奔潰。
解決這個問題可以從以下方面入手:

  • 保證緩存的高可用。使用redis的集群模式,即使個別redis節(jié)點下線,緩存還是可以用。一般稍微大點的公司還可能會在多個機房部署Redis。
    這樣即使某個機房突然停電,或者光纖又被挖斷了,這時候緩存還是可以使用。
  • 使用多級緩存。不同級別緩存時間過時時間不一樣,即使某個級別緩存過期了,還有其他緩存級別
    兜底。比如我們Redis緩存過期了,我們還有本地緩存。這樣的話即使沒有命中redis,有可能會命中本地緩存。
  • 緩存永不過期。Redis中保存的key永久不失效,這樣的話就不會出現(xiàn)大量緩存同時失效的問題,但是這種做法會浪費更多的存儲空間,一般應(yīng)該也不會推薦這種做法。
  • 使用隨機過期時間。為每一個key都合理的設(shè)計一個過期時間,這樣可以避免大量的key再同一時刻集體失效。
  • 異步重建緩存。這樣的話需要維護每個key的過期時間,定時去輪詢這些key的過期時間。例如一個keyvalue設(shè)置的過期時間是30min,那我們可以為這個key設(shè)置它自己的一個過期時間為20min。所以當這個key到了20min的時候我們就可以重新去構(gòu)建這個key的緩存,同時也更新這個key的一個過期時間。
緩存穿透

指查詢一個不存在的數(shù)據(jù),每次通過接口或者去查詢數(shù)據(jù)庫都查不到這個數(shù)據(jù),比如黑客的惡意攻擊,比如知道一個訂單號后,然后就偽造一些不存在的訂單號,然后并發(fā)來請求你這個訂單詳情。這些訂單號在緩存中都查詢不到,然后會導致把這些查詢請求全部打到數(shù)據(jù)庫或者SOA接口。這樣的話就會導致數(shù)據(jù)庫宕機或者你的服務(wù)大量超時。
這種查詢不存在的數(shù)據(jù)就是緩存擊穿。
解決這個問題可以從以下方面入手:

  • 緩存空值,對于這些不存在的請求,仍然給它緩存一個空的結(jié)果,這種方式簡單粗暴,但是如果后續(xù)這個請求有新值了需要把原來緩存的空值刪除掉(所以一般過期時間可以稍微設(shè)置的比較短)。
  • 通過布隆過濾器。查詢緩存之前先去布隆過濾器查詢下這個數(shù)據(jù)是否存在。如果數(shù)據(jù)不存在,然后直接返回空。這樣的話也會減少底層系統(tǒng)的查詢壓力。
  • 緩存沒有直接返回。 這種方式的話要根據(jù)自己的實際業(yè)務(wù)來進行選擇。比如固定的數(shù)據(jù),一些省份信息或者城市信息,可以全部緩存起來。這樣的話數(shù)據(jù)有變化的情況,緩存也需要跟著變化。實現(xiàn)起來可能比較復雜。
緩存擊穿

是指緩存里面的一個熱點key(拼多多的五菱宏光神車的秒殺)在某個時間點過期。針對于這一個key有大量并發(fā)請求過來然后都會同時去數(shù)據(jù)庫請求數(shù)據(jù),瞬間對數(shù)據(jù)庫造成巨大的壓力。
這個的話可以用緩存雪崩的幾種解決方法來避免:

  • 緩存永不過期。Redis中保存的key永久不失效,這樣的話就不會出現(xiàn)大量緩存同時失效的問題,但是這種做法會浪費更多的存儲空間,一般應(yīng)該也不會推薦這種做法。
  • 異步重建緩存。這樣的話需要維護每個key的過期時間,定時去輪詢這些key的過期時間。例如一個keyvalue設(shè)置的過期時間是30min,那我們可以為這個key設(shè)置它自己的一個過期時間為20min。所以當這個key到了20min的時候我們就可以重新去構(gòu)建這個key的緩存,同時也更新這個key的一個過期時間。
  • 互斥鎖重建緩存。這種情況的話只能針對于同一個key的情況下,比如你有100個并發(fā)請求都要來取A的緩存,這時候我們可以借助redis分布式鎖來構(gòu)建緩存,讓只有一個請求可以去查詢DB其他99個(沒有獲取到鎖)都在外面等著,等A查詢到數(shù)據(jù)并且把緩存構(gòu)建好之后其他99個請求都只需要從緩存取就好了。原理就跟我們javaDCL(double checked locking)思想有點類似。
    在這里插入圖片描述

緩存更新

我們一般的緩存更新主要有以下幾種更新策略:

  • 先更新緩存,再更新數(shù)據(jù)庫
  • 先更新數(shù)據(jù)庫,再更新緩存
  • 先刪除緩存,再更新數(shù)據(jù)庫
  • 先更新數(shù)據(jù)源庫,再刪除緩存
    至于選擇哪種更新策略的話,沒有絕對的選擇,可以根據(jù)自己的業(yè)務(wù)情況來選擇適合自己的不過一般推薦的話是選擇 先更新數(shù)據(jù)源庫,再刪除緩存。關(guān)于這幾種更新的介紹可以推薦大家看下博客園大佬孤獨煙寫的《分布式之數(shù)據(jù)庫和緩存雙寫一致性方案解析》這一篇文章,看完文章評論也可以去看看,評論跟內(nèi)容一樣精彩。

總結(jié)

如果想要真正的設(shè)計好一個緩存,我們還是必須要掌握很多的知識,對于不同場景,緩存有各自不同的用法。比如實際工作中我們對于訂單詳情的一個緩存。我們可能會根據(jù)訂單的狀態(tài)來來構(gòu)建緩存。我們就以機票訂單為例,已出行、或者已經(jīng)取消的訂單我們基本上是不會去管的(訂單狀態(tài)已經(jīng)終止了),這種的話數(shù)據(jù)基本也不會變了,所以對于這種訂單我們設(shè)置的過期時間是不是就可以久一點,比如7天或者30天。對于未出行即將起飛的訂單,這時候顧客是不是就會頻繁的去刷新訂單看看,看看有沒有晚點什么的,或者登機口是在哪。對于這種實時性要求比較高的訂單我們過期時間還是要設(shè)置的比較短的,如果是需要更改訂單的狀態(tài)查詢的時候可以直接不走緩存,直接查詢master庫。畢竟這種更改訂單狀態(tài)的操作還是比較有限的。大多數(shù)情況都是用來展示的。展示的話是可以允許實時性要求沒那么高??偟膩碚f需要開具體的業(yè)務(wù),沒有通用的方案??茨愕臉I(yè)務(wù)需求的容忍度,畢竟脫離了業(yè)務(wù)來談技術(shù)都是耍流氓,是業(yè)務(wù)驅(qū)動技術(shù)。

結(jié)束

  • 由于自己才疏學淺,難免會有紕漏,假如你發(fā)現(xiàn)了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉(zhuǎn)發(fā)、分享、贊賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎并感謝您的關(guān)注。

站在巨人的肩膀上摘蘋果:
https://juejin.im/post/6844903665845665805
https://tech.meituan.com/2017/03/17/cache-about.html
https://www.cnblogs.com/rjzheng/p/9041659.html#!comments

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容