[億級流量網站架構讀後記錄二、緩存篇]
高并發
緩存
作用即讓資料更接近于使用者, 目的是讓通路速度更快.
緩存命中率
從緩存中讀取資料的次數與總讀取次數的比率.
緩存回收政策
- 基于空間, 空間達到上限按照政策回收.
- 基于容量, 緩存條目數量達到上限…
- 基于時間, TTL(Time To Live), 存活達到一定時間…; TTI(Time To Idle), 空閑達到一定時間…
- 基于java對象引用, 比如軟弱引用.
- 回收算法, 使用基于空間和容量的緩存會使用一定的政策移除舊資料, 常見如下:
- FIFO(First In First Out): 先進先出算法, 即先放入緩存的先被移除.
- LRU(Least Recently Used): 最近最少使用算法, 使用時間距離現在最久的那個被删除
- LFU(Least Frequently Used): 最不常用算法, 一定時間段内使用頻率最少的被移除
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進行基準性能測試。