[億級流量網站架構讀後記錄二、緩存篇]

[億級流量網站架構讀後記錄二、緩存篇]

高併發

緩存

​ 作用即讓數據更接近於使用者, 目的是讓訪問速度更快.

緩存命中率

​ 從緩存中讀取數據的次數與總讀取次數的比率.

緩存回收策略

  • 基於空間, 空間達到上限按照策略回收.

  • 基於容量, 緩存條目數量達到上限…

  • 基於時間, TTL(Time To Live), 存活達到一定時間…; TTI(Time To Idle), 空閒達到一定時間…

  • 基於java對象引用, 比如軟弱引用.

  • 回收算法, 使用基於空間和容量的緩存會使用一定的策略移除舊數據, 常見如下:

    • FIFO(First In First Out): 先進先出算法, 即先放入緩存的先被移除.

    • LRU(Least Recently Used): 最近最少使用算法, 使用時間距離現在最久的那個被刪除

    • LFU(Least Frequently Used): 最不常用算法, 一定時間段內使用頻率最少的被移除

    實際應用中基於LRU的緩存居多, 如Guava Cache, Ehcache 支持 LRU.

java 緩存類型

  • 堆內存: 使用java堆內存來存儲對象. 好處是不需要序列化和反序列化, 是最快的緩存.缺點就是當緩存數據量很大時, GC暫停時間會很長, 存儲容量受限於堆空間大小. 一般使用軟/弱引用來存儲. 如Guava cache, Ehcache 3.x, MapDB實現.
  • 堆外內存: 即緩存數據存儲在堆外內存, 可減少GC時間, 可以支持更大的緩存空間. 但是需要序列化.所以會比堆緩存慢得多.
  • 磁盤緩存: 即緩存數據存儲在磁盤上, 當JVM重啓數據還是存在的, 而堆內存和堆外緩存數據會丟失, 需要重新加載. 可以使用Ehcache 3.x, MapDB實現.
  • 分佈式緩存: 上邊的緩存是進程內緩存和磁盤緩存, 在多JVM實例下, 會存在兩個問題: 1.單機容量問題; 2.數據一致性問題(多臺JVM實例的緩存數據不一致怎麼辦? 可以設置數據的過期時間定時更新數據); 3.緩存不命中時, 需要回溯到DB/服務請求多變問題, 每個實例在緩存不命中的情況下都會回溯到DB加載數據, 因此整體對DB的訪問就變多了, 解決辦法是使用一致性哈希分片算法. 因此, 要考慮使用分佈式緩存.

兩種模式如下:

  • 單機時: 存儲最熱的數據到堆緩存, 相對熱的數據到堆外緩存, 不熱的數據到磁盤緩存.
  • 集羣時: 存儲最熱的數據到堆緩存, 相對熱的數據到堆外緩存, 全量數據到分佈式緩存.

技術舉例:

​ Guava Cache 只提供堆緩存, 小巧靈活, 性能最好, 如果只使用堆緩存, 就它了.

​ Ehcache 3.x 提供了堆緩存, 堆外緩存, 磁盤緩存, 分佈式緩存. 但是, 這個版本代碼註釋比較少, API 功能還不完善. 如果需要穩定的API和功能, 考慮使用2.x.

​ MapDB 是一款嵌入式Java數據庫引擎和集合框架. 提供了Maps, Sets, Lists, Queues, Bitmaps的支持, 還支持ACID事務, 增量備份. 支持堆緩存, 堆外緩存, 磁盤緩存.

應用級緩存示例

  • 多級緩存API封裝

    • 本地緩存初始化: 本地緩存過期時間使用分佈式緩存過期時間的一半, 防止本地緩存數據緩存時間太長造成多實例間的數據不一致; 另外, 將緩存key前綴與本地緩存關聯, 從而匹配緩存key前綴, 就可以找到相關聯的本地的緩存.
    • 寫緩存: 先寫本地緩存, 如果需要寫分佈式緩存, 則通過異步更新分佈式緩存.
    • 讀緩存: 先讀本地緩存, 本地不命中再批量查詢分佈式緩存, 在查詢分佈式緩存時通過分區批量查詢(即將key分頁查詢).
  • NULL Cache: 當DB沒有數據時, 寫入NULL對象到緩存. 讀取數據時, 如果發現NULL對象, 則返回null, 而不是回源到DB. 通過這種方式可防止當key對應的數據在DB中不存在時頻繁查詢DB的情況.

  • 強制獲取最新數據: 可通過ThreadLocal開關來決定是否強制刷新緩存.

  • 失敗統計

  • 延遲報警(不能頻繁報警, 可考慮N久報警了M次)

  • 緩存使用模式實踐

    ​ 前人總結好的模式, 主要分爲兩大類: Cache-Aside和Cache-As-SoR(Read-through、Write-through、Write-behind)。

    ​ SoR(system-of-record):記錄系統,或者可以叫做數據源。

    ​ Cache: 緩存,是SoR的快照數據,Cache的訪問速度比SoR要快,放入Cache的目的是提升訪問速度,減少回源到SoR的次數。

    ​ 回源:即回到數據源頭獲取數據。

    ​ Cache-Aside:即業務代碼圍繞Cache寫,比如讀取緩存,不存在則回源。適合AOP實現。可能存在併發更新的情況:

    • 如果是用戶維度的數據,這種機率非常小,可以不考慮,加上過期時間來解決即可。
    • 對於如商品這種基礎數據,可考慮使用cannal訂閱binlog,來進行增量更新分佈式緩存,這樣不會存在緩存數據不一致的情況。但是,緩存更新會存在延遲。而本地緩存可根據不一致容忍度設置合理的過期時間。
    • 讀服務場景,可以考慮使用一致性哈希,將相同的操作負載均衡到同一個實例,從而減少併發機率。或者設置比較短的過期時間。

    ​ Cache-As-SoR:即把Cache看作SoR,所有操作針對Cache進行,然後Cache再委託給SoR進行真實的讀寫。即業務代碼中只看到Cache的操作。看不到SoR的操作。有三種實現:read-through、write-through、write-behind。

    ​ Read-through:業務代碼首先調用Cache,如果Cache不命中由Cache回源到SoR,而不是業務代碼。Guava cache和Ehcache 3.x都支持該模式。好處是,應用業務代碼更簡潔了,沒有重複代碼;解決Dog-pile effect,即當某個緩存失效時,又有大量相同的請求沒命中緩存,從而使請求同時到後端,導致後端壓力太大,此時限定一個請求去拿即可。

    ​ Write-Through:稱爲穿透寫模式/直寫模式–業務代碼首先調用Cache寫(新增/修改),然後由Cache負責寫緩存和寫SoR。目前只有Ehcache 3.x支持。

    ​ Write-Behind:也叫Wrtie-Back,即回寫模式。不同於Write-Through是同步寫SoR和Cache,Write-Behind是異步寫。異步寫可實現批量寫,合併寫,延時和限流。

  • Copy Pattern

    ​ 有兩種Copy Pattern,Copy-On-Read(在讀時複製)和Copy-On-Write(在寫時複製),在Guava Cache和Ehcache中堆緩存都是基於引用的,這樣如果有人拿到緩存數據並修改,則發生不可預測的問題。Ehcache 3.x提供了支持。

  • 性能測試,可使用JMH1.4進行基準性能測試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章