資料庫減負刻不容緩?多級快取設計瞭解一下!

李雪薇發表於2018-08-02

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


為什麼要設計快取呢?


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


傳統的cpu透過fsb直連記憶體的方式顯然就會因為記憶體訪問的等待,導致cpu吞吐量下降,記憶體成為效能瓶頸。同時又由於記憶體訪問的熱點資料集中性,所以需要在cpu與記憶體之間做一層臨時的儲存器作為快取記憶體。


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

undefined

故而應用於SOA甚至微服務的場景,記憶體相當於儲存業務資料的持久化資料庫,其吞吐量肯定是遠遠小於快取的,而對於java程式來講,本地的jvm快取優於集中式的redis快取。


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

 

給資料庫減負  


快取分散式,做多級快取

 

undefined


1、讀請求時寫快取


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


  1. 不要複製貼上,避免重複程式碼

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

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


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


 

2、寫快取失敗了怎麼辦?應該先寫快取還是資料庫呢?


既然是快取的設計,那麼策略一定是保證最終一致性,那麼我們只需要採用非同步訊息來補償就好了。


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


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

 

3、關於其他效能方面


快取設計都是佔用越少越好,記憶體資源昂貴以及太大不好維護都驅使我們這樣設計。所以要儘可能減少快取不必要的資料,有的同學圖省事把整個物件序列化儲存。另外,序列化與反序列化也是消耗效能的。


  vs各種快取同步方案


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


1、懶載入式


如上段提到的方式,讀時順便載入。為了更新快取資料,需要過期快取。


undefined

                     


優點 :簡單直接


缺點

  • 會造成一次快取不命中

  • 這樣當使用者併發很大時,恰好快取中無資料,資料庫承擔瞬時流量過大會造成風險。


懶載入式太簡單了,沒有自動載入,非同步重新整理等機制,為了彌補其缺陷,請參見接下來的兩種方法。


2、補充式


可以在快取時,把過期時間等資訊寫到一個非同步佇列裡,後臺起個執行緒池定期掃描這個佇列,在快過期時主動reload快取,使得資料會一直保持在快取中,如果快取沒有也沒有必要去資料庫查詢了。常見的處理方式有使用binlog加工成訊息供增量處理。


undefined


  • 優點 :重新整理快取變為非同步的任務,對資料庫的壓力瞬間由於任務佇列的介入而降低了,削平併發的波峰。

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

 

3、定時載入式


這就需要有個非同步執行緒池定期把資料庫的資料刷到集中式快取,如redis裡。


  • 優點 :保證所有資料最小時間差同步到快取中,延遲很低。

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

  • ps :為了防止佇列過大排程出現問題,處理完的資料要儘快結轉,且要對積壓資料以及寫入情況做監控。

 

防止快取穿透


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


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

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

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

 

熱點快取與快取淘汰策略


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


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


1、FIFO(First In,First Out)


先進先出,淘汰最早進來的快取資料,一個標準的佇列。


undefined


以佇列為基本資料結構,從隊首進入新資料,從隊尾淘汰。

 

2、LRU(Least RecentlyUsed)


最近最少使用,淘汰最近不使用的快取資料。如果資料最近被訪問過,則不淘汰。



  1. 和FIFO不同的是,需要對連結串列做基本模型,讀寫的時間複雜度是O(1),寫入新資料進入頭部,連結串列滿了資料從尾部淘汰;

  2. 最近時間被訪問的資料移動到頭部,實現演算法有很多,如hashmap+雙向連結串列等等;

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

 

3、LFU(Least Frequently used)


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


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

  2. 因為新加入的資料訪問次數為1,所以插入到佇列尾部;

  3. 佇列中的資料被新訪問後,引用計數增加,佇列重新排序;

  4. 當需要淘汰資料時,將已經排序的列表最後的資料塊刪除;

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

 

快取 使用的一些常見問題


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

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

 

Q:怎麼批次更新一批快取資料?

A:依次從資料庫讀取,然後批次寫入快取,批次更新,設定版本過期key或者主動刪除。

 

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

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

 

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

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


作者 | 王梓晨

 轉自 | 京東技術


京東物流研發部架構師,GIS技術部負責人,2012年加入京東,多年一線團隊大促備戰經驗,負責物流研發一些部門的架構工作,專注於低延遲系統設計與海量資料處理。曾負責青龍配送分單團隊,主導重構架構設計與主要研發工作,短期內提升了服務效能數十倍。還設計研發了地址配送網點分類模型,實現了配送到路區的精準化分單,降本增效,大幅提升了自動分單準確率。目前負責物流GIS部門,先後主導了國標轉京標、物流視覺化等專案。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31509936/viewspace-2168946/,如需轉載,請註明出處,否則將追究法律責任。

相關文章