[億級流量網站架構讀後記錄二、緩存篇]
高併發
緩存
作用即讓數據更接近於使用者, 目的是讓訪問速度更快.
緩存命中率
從緩存中讀取數據的次數與總讀取次數的比率.
緩存回收策略
基於空間, 空間達到上限按照策略回收.
基於容量, 緩存條目數量達到上限…
基於時間, 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進行基準性能測試。