巧妙設計多級緩存,爲數據庫減負

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

1、爲何要設計緩存?算法

其實高併發應對的解決方案不是互聯網首創的,計算機先祖們很早就對相似的場景作了方案。好比《計算機組成原理》這樣提到的CPU緩存概念:它是一種高速緩存,容量比內存小可是速度卻快不少,這種緩存的出現主要是爲了解決CPU運算速度遠大於內存讀寫速度,甚至達到千萬倍的問題。數據庫

傳統的CPU經過fsb直連內存的方式顯然就會由於內存訪問的等待,致使CPU吞吐量降低,內存成爲性能瓶頸。同時又因爲內存訪問的熱點數據集中性,因此須要在CPU與內存之間作一層臨時的存儲器做爲高速緩存。緩存

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

故而應用於SOA甚至微服務的場景,內存至關於存儲業務數據的持久化數據庫,其吞吐量確定是遠遠小於緩存的,而對於java程序來說,本地的JVM緩存優於集中式的Redis緩存。併發

關係型數據庫操做方便、易於維護且訪問數據靈活,可是隨着數據量的增長,其檢索、更新的效率會愈來愈低。因此在高併發低延遲要求複雜的場景,要給數據庫減負,減小其壓力。框架

2、給數據庫減負異步

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算法不常常單獨使用而是組合在其餘策略中使用。

5.緩存使用的一些常見問題

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

A1:首先看數據量,看緩存更新的成本,若是總體緩存數據量不是很大,並且變化的不頻繁,那麼建議本地緩存。

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

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

Q3:若是不知道有哪些key怎麼按期刪除?

A3:拿Redis來講keys * 太損耗性能,不推薦。能夠指定一個集合,把全部的key都存到這個集合裏,而後對整個集合進行刪除,這樣便能徹底清理了。

Q4:一個key包含的集合很大,Redis沒法作到內存空間上的均勻Shard?

A4:一、能夠簡單的設置key過時,這樣就要容許有緩存不命中的狀況;二、給key設置版本,好比爲兩天後的當前時間,而後讀取緩存時用時間判斷一下是否須要從新加載緩存,做爲版本過時的策略。

相關文章
相關標籤/搜索