多級緩存設計詳解|給資料庫減負,刻不容緩!

多級緩存設計詳解|給數據庫減負,刻不容緩!

自古兵家多謀,《謀攻篇》,“故上兵伐謀,其次伐交,其次伐兵,其下攻城。攻城之法,為不得已”,可見攻城之計有很多種,而爬牆攻城是最不明智的做法,軍隊疲憊受損、錢糧損耗、百姓遭殃。故而我們有很多迂迴之策,謀略、外交、軍事手段等等,每一種都比攻城的代價小,更輕量級,緩存設計亦是如此。

為什麼要設計緩存呢?

其實高併發應對的解決方案不是互聯網獨創的,計算機先祖們很早就對類似的場景做了方案。比如《計算機組成原理》這樣提到的cpu緩存概念,它是一種高速緩存,容量比內存小但是速度卻快很多,這種緩存的出現主要是為了解決cpu運算速度遠大於內存讀寫速度,甚至達到千萬倍。

傳統的cpu通過fsb直連內存的方式顯然就會因為內存訪問的等待,導致cpu吞吐量下降,內存成為性能瓶頸。同時又由於內存訪問的熱點數據集中性,所以需要在cpu與內存之間做一層臨時的存儲器作為高速緩存。

隨著系統複雜性的提升,這種高速緩存和內存之間的速度進一步拉開,由於技術難度和成本等原因,所以有了更大的二級、三級緩存。根據讀取順序,絕大多數的請求首先落在一級緩存上,其次二級...

多級緩存設計詳解|給數據庫減負,刻不容緩!

故而應用於SOA甚至微服務的場景,內存相當於存儲業務數據的持久化數據庫,其吞吐量肯定是遠遠小於緩存的,而對於java程序來講,本地的jvm緩存優於集中式的redis緩存。

關係型數據庫操作方便、易於維護且訪問數據靈活,但是隨著數據量的增加,其檢索、更新的效率會越來越低。所以在高併發低延遲要求複雜的場景,要給數據庫減負,減少其壓力。

給數據庫減負

1、緩存分佈式,做多級緩存

多級緩存設計詳解|給數據庫減負,刻不容緩!

讀請求時寫緩存

寫緩存時一級一級寫,先寫本地緩存,再寫集中式緩存。具體些緩存的方法可以有很多種,但是需要注意幾項原則:

不要複製粘貼,避免重複代碼

切忌和業務耦合太緊,不利於後期維護

開發初期剛剛上線階段,為了排查問題,常常會給緩存設置開關,但是開關設置多了則會同時升高系統的複雜度,需要結合一套統一配置管理系統,京東物流有一套叫做UCC,且聽下回分解......

多級緩存設計詳解|給數據庫減負,刻不容緩!

綜上所述,高耦合帶來的痛,彌補的代價是很大的,所以可以借鑑Spring cache來實現,實現也比較簡單,使用時一個註解就搞定了。

多級緩存設計詳解|給數據庫減負,刻不容緩!

寫緩存失敗了怎麼辦?應該先寫緩存還是數據庫呢?

既然是緩存的設計,那麼策略一定是保證最終一致性,那麼我們只需要採用異步消息來補償就好了。

大部分緩存應用的場景是讀寫比差異很大的,讀遠大於寫,在這種場景下,只需要以數據庫為主,先寫數據庫,再寫緩存就好了。

最後補充一點,數據庫出現異常時,不要一股腦的catch RuntimeException,而是把具體關心的異常往外拋,然後進行有針對性的異常處理。

關於其他性能方面

緩存設計都是佔用越少越好,內存資源昂貴以及太大不好維護都驅使我們這樣設計。所以要儘可能減少緩存不必要的數據,有的同學圖省事把整個對象序列化存儲。另外,序列化與反序列化也是消耗性能的。

2、vs各種緩存同步方案

緩存同步方案有很多種,在考慮一致性、數據庫訪問壓力、實時性等方面做權衡。總的來說有以下幾種方式:

懶加載式

如上段提到的方式,讀時順便加載。為了更新緩存數據,需要過期緩存。

多級緩存設計詳解|給數據庫減負,刻不容緩!

優點:簡單直接

缺點:

會造成一次緩存不命中

這樣當用戶併發很大時,恰好緩存中無數據,數據庫承擔瞬時流量過大會造成風險。

懶加載式太簡單了,沒有自動加載,異步刷新等機制,為了彌補其缺陷,請參見接下來的兩種方法。

補充式

可以在緩存時,把過期時間等信息寫到一個異步隊列裡,後臺起個線程池定期掃描這個隊列,在快過期時主動reload緩存,使得數據會一直保持在緩存中,如果緩存沒有也沒有必要去數據庫查詢了。常見的處理方式有使用binlog加工成消息供增量處理。

多級緩存設計詳解|給數據庫減負,刻不容緩!

優點:刷新緩存變為異步的任務,對數據庫的壓力瞬間由於任務隊列的介入而降低了,削平併發的波峰。

缺點:消息一旦積壓會造成同步延遲,引入複雜度。

定時加載式

這就需要有個異步線程池定期把數據庫的數據刷到集中式緩存,如redis裡。

優點:保證所有數據最小時間差同步到緩存中,延遲很低。

缺點:如補充式,需要一個任務調度框架,複雜度提升,且要保證任務的順序。如果遞進一步還想加載到本地緩存,就得本地應用自己起線程抓取,方案維護成本高。可以考慮使用mq或者其他異步任務調度框架。

ps:為了防止隊列過大調度出現問題,處理完的數據要儘快結轉,且要對積壓數據以及寫入情況做監控。

3、防止緩存穿透

緩存穿透是指查詢的key壓根不存在,從而緩存查詢不到而查詢了數據庫。若是這樣的key恰好併發請求很大,那麼就會對數據庫造成不必要的壓力。怎麼解決呢?

把所有存在的key都存到另外一個存儲的Set集合裡,查詢時可以先查詢key是否存在。

乾脆簡單一些,給查詢不到的key也加一個標識空值的Value,這樣就不會去查詢數據庫了,比如場景為查詢省市區街道對應的移動營業廳,若是某街道確實沒有移動營業廳,key規則不變,value可以設置為"0"等無意義的字符。當然此種方案要保證緩存集群的高可用。

這些Key可能不是永遠不存在,所以需要根據業務場景來設置過期時間。

4、熱點緩存與緩存淘汰策略

有一些場景,需要只保持一部分的熱點緩存,不需要全量緩存,比如熱賣的商品信息,購買某類商品的熱門商圈信息等等。

綜合來講,緩存過期的策略有以下三種:

FIFO(First In,First Out)

先進先出,淘汰最早進來的緩存數據,一個標準的隊列。

多級緩存設計詳解|給數據庫減負,刻不容緩!

以隊列為基本數據結構,從隊首進入新數據,從隊尾淘汰。

LRU(Least RecentlyUsed)

最近最少使用,淘汰最近不使用的緩存數據。如果數據最近被訪問過,則不淘汰。

多級緩存設計詳解|給數據庫減負,刻不容緩!

和FIFO不同的是,需要對鏈表做基本模型,讀寫的時間複雜度是O(1),寫入新數據進入頭部,鏈表滿了數據從尾部淘汰;

最近時間被訪問的數據移動到頭部,實現算法有很多,如hashmap+雙向鏈表等等;

問題在於若是偶發性某些key被最近頻繁訪問,而非常態,則數據受到汙染。

LFU(Least Frequently used)

最近使用次數最少的數據被淘汰,注意和LRU的區別在於LRU的淘汰規則是基於訪問時間。

多級緩存設計詳解|給數據庫減負,刻不容緩!

LFU中的每個數據塊都有一個引用計數,數據塊按照引用計數排序,若是恰好具有相同引用計數的數據塊則按照時間排序;

因為新加入的數據訪問次數為1,所以插入到隊列尾部;

隊列中的數據被新訪問後,引用計數增加,隊列重新排序;

當需要淘汰數據時,將已經排序的列表最後的數據塊刪除;

有很明顯問題是若短時間內被頻繁訪問多次,比如訪問異常或者循環沒有控制住,而後很長時間未使用,則此數據會因為頻率高而被錯誤的保留下來沒有被淘汰。尤其對於新來的數據,由於其起始的次數是1,所以即便被正常使用也會因為比不過老的數據而被淘汰。所以維基百科說純粹的LFU算法不經常單獨使用而是組合在其他策略中使用。

4、緩存使用的一些常見問題

Q:那麼應該選擇用本地緩存(local cache)還是集中式緩存(Cache cluster)呢?

A:首先看數據量,看緩存更新的成本,如果整體緩存數據量不是很大,而且變化的不頻繁,那麼建議本地緩存。

Q:怎麼批量更新一批緩存數據?

A:依次從數據庫讀取,然後批量寫入緩存,批量更新,設置版本過期key或者主動刪除。

Q:如果不知道有哪些key怎麼定期刪除?

A:拿redis來說keys * 太損耗性能,不推薦。可以指定一個集合,把所有的key都存到這個集合裡,然後對整個集合進行刪除,這樣便能完全清理了。

Q:一個key包含的集合很大,redis無法做到內存空間上的均勻Shard?

A:1、可以簡單的設置key過期,這樣就要允許有緩存不命中的情況;2、給key設置版本,比如為兩天後的當前時間,然後讀取緩存時用時間判斷一下是否需要重新加載緩存,作為版本過期的策略。

對Java微服務、分佈式、高併發、高可用、大型互聯網架構技術、面試經驗交流感興趣的。可以關注我的頭條號,我會在微頭條不定期的發放免費的資料鏈接,這些資料都是從各個技術網站蒐集、整理出來的,如果你有好的學習資料可以私聊發我,我會註明出處之後分享給大家。歡迎分享,歡迎評論,歡迎轉發!


分享到:


相關文章: