系統設計 - 應用系統緩存
在應用系統中,使用緩存不算非常難的事,但是設計好一套緩存策略比較麻煩,這樣既能起到好的緩存效果也能在合適的時候更新緩存。
今天我們聊聊緩存。
不過,在計算機科學中,有很多種緩存。我們先聊聊緩存的類型,再談應用系統中的緩存。
我們會涉及哪些緩存知識?
這裏我把緩存分爲兩類:
-
非應用緩存:難以被程序員干預、控制和使用的緩存,但是我們能通過選擇合適的數據結構,寫出緩存友好的代碼。
-
應用緩存:容易被程序員干預、控制和使用的緩存。
非應用緩存有:
-
CPU 緩存。例如,使用數組的 index 取數會比指針取數更快,相關指令更容易被 CPU 緩存。
-
磁盤緩存。儘量通過流的方式來讀取磁盤文件,另外也應該避免不必要的 flush 操作。
-
數據庫緩存。數據庫常見的緩存有 SQL 緩存和查詢緩存,前者只是緩存 SQL 的解析結果,後者會把結果集也緩存下來,一般需要手動開啓。
-
ORM 緩存。ORM 會在同一會話中對多次查詢進行緩存。
-
網關緩存或者服務商緩存。例如,Nginx 等網絡服務器緩存,經常會出現一些問題。
-
DNS 緩存。DNS 解析也會被緩存下來。
-
CDN 緩存。通過網絡服務商城域網對靜態資源進行緩存。
非應用緩存通常不會侵入業務,比較透明,我們需要知道它們存在必要時做出配置。另外,對程序員來說更多的關心應用緩存,這些緩存往往和業務相關。
應用緩存有:
-
內存本地緩存。我們可以使用 WeakHashMap、Guava、Caffeine、EVcache 等方法直接緩存到內存。甚至直接放到對象靜態屬性上作爲單例值。違反認知的地方是,很多地方應該大量使用本地緩存,避免一上來直接使用 Redis 緩存。它的適用場景有:不需要多個實例保持一致性,緩存數據量大,不需要外部失效,需要高速存取;單次請求或者上下文中的數據。
-
分佈式緩存。分佈式緩存使用的場景是需要多個實例保持一致性,且規模比較大避免佔用應用服務的內存。常見的技術方案有:Redis、Memcached、Tair 等。
-
計算緩存。除了對數據進行緩存外,還可以對計算結果進行緩存,用了節省 CPU 時間。例如遞歸的記憶化。
-
前端緩存。利用設置 HTTP 頭信息將數據緩存到瀏覽器上。
通常來說,分佈式緩存需要一套有效的緩存策略。回答:緩存哪種類型的對象?緩存的顆粒度如何?什麼時候去更新?
緩存對象顆粒度
對於後端服務來說,根據分層會有不同的 POJO(API 返回對象、領域對象、數據庫 PO),我們緩存什麼呢?實際項目中這幾種情況都會有。
爲了取得最好的緩存效果(命中率高,手動失效少),需要權衡被緩存對象的顆粒度。
緩存 API 返回對象(Response)
如果以 Response 爲粒度,其實是以用例爲視角。比如訂單詳情,需要組裝非常多的數據,且變化不劇烈。
特點是:
-
顆粒度大,緩存的效果好(納入緩存的內容多,包括數據和計算邏輯)。
-
命中概率低,組成 key 的條件太多。
-
更新策略不好控制,開發難度比較大,需要加很多代碼,需要看業務是否能接受,舉個例子:用戶詳情,使用用戶 ID 做 Key,相關地方都需要手動刷新,例如地址、積分、消費記錄等。
-
部分電商公司使用該方案,互聯網場中景,用戶基數比較大,併發請求高,取數代價高。
-
緩存 key 一般是 URL 中關鍵路徑信息。
緩存領域對象聚合
如果使用 DDD 分層,有聚合概念,可以以聚合粒度緩存。
其特點是:
-
顆粒度適中,緩存的效果適中。
-
根據聚合根來控制緩存失效。
-
部分數據可能不會被納入緩存,因爲組裝爲 Response 的過程不會被緩存。
-
依賴 DDD 的取數邏輯,有時候爲整存整取。
-
緩存 key 一般是聚合根 ID。
如果使用 Mybatis、Mybatis Plus 一般會定義自己的 PO 對象,所以可以單獨處理緩存策略。
它的特點是:
-
顆粒度小,緩存的效果小。
-
可以藉助 ORM 框架緩存,Session 內多次獲取,可以避免再查詢,需要開啓二級緩存。
-
緩存 key 一般是表的主鍵。
一般來說,緩存顆粒度越小,失效策略越好處理,但是緩存住的數據和邏輯就越少,在有些場景下不能滿足我們的期望。
另外,還有一些特殊場景的緩存。
-
列表頁查詢緩存。因爲很難觸發更新策略,一般不加緩存,直接走讀庫 CQRS 模式,或者走 ES,推薦 ES 的更新策略爲 COW(Copy On Write)。在條件業務允許時,也可以根據查詢條件做很短過期的查詢。
-
大 key。大 key 存在反而會導致性能瓶頸,業界 10k 以上會被叫做大 key,需要對 key 進行拆分緩存即可。
-
寫緩存。一般不對寫進行緩存,但是一些高併發的場景,會將寫數據放入 Redis 異步寫入,也可以看做一種緩存。
一般來說:推薦使用聚合緩存;列表不緩存,使用讀寫分離從庫查詢;更新均不做緩存,只對單個查緩存。
緩存設計注意事項
緩存雪崩、緩存擊穿、緩存穿透
緩存雪崩是指在某一個時刻突然緩存都失效了。原因有兩種,一種是緩存服務器宕機了,流量全部進入數據庫;另外一種情況是在同一時刻失效了。
對於前者可以通過熔斷、高可用等設計,而後者需要對緩存過期時間加一個隨機偏移值,避免同時失效。
緩存擊穿和雪崩有點類似,業界往往說的是系統健康運行依賴某些熱點 key 的緩存,當這些熱點 key 失效後流量全部打到數據庫上。
爲了保證熱點 key 安全,在一些關鍵系統甚至會用多套 Redis 分級處理,或者將其設置爲永不過期,通過程序觸發更新的方式保證服務可用性。這種思想有點以前 CMS 站點的靜態化,將動態頁面輸出爲 HTML 頁面靜態化。
緩存穿透常常說的是,明明有 Redis 緩存但是偏偏大部分都不能命中進入到數據庫。有時候是因爲 key 設計不合理,導致命中率很低,其它情況有可能是遇到爬蟲或者攻擊,製造了大量的無效參數,這些參數不會命中緩存直接進入了數據庫查詢階段。
如果頻繁發生緩存穿透,剛好條件又合適可以採用返回空對象,避免回源到數據庫。
緩存更新策略
我們一般不會主動更新緩存,而是讓其失效,在下一次取數時如果沒有緩存則更新緩存。
緩存更新在不同場景下有幾種策略:
-
自然過期:通過時間作爲自然過期策略。
-
主動失效:進程內可以通過註解實現,在合適的更新場景觸發相關緩存失效;進程間可以通過 MQ 實現封裝一個分佈式的失效註解。
-
主動預熱:使用腳本,在服務上線後跑一遍熱點數據進行預熱。
需要緩存的常見場景
-
熱點數據:用戶信息、權限數據、配置表或者元數據。
-
高價值數據:目錄樹、機構樹、DashBoard 統計值。
-
大 I/O 數據:前端緩存。
序列化和反序列化坑
-
不推薦使用 Java 自帶的序列化。
-
推薦將對象序列化爲 JSON,但是不要使用帶類型信息的格式,否則在包調整後反序列化會失敗。
-
上線後最好清掉緩存,重新預熱,否則會出現各種反序列化問題。
如何寫出方便緩存的代碼?
緩存友好的代碼,其本質是容易找到一個標識標記這組數據,這也是爲什麼列表頁不適合緩存的原因。
-
命令和查詢分離,對狀態更新的操作和返回數據結果的操作不要使用同一個方法。
-
少用魔法,儘量不使用 IOP 自動填充值或者組裝數據
-
儘量使用參數表傳參,少用對象傳參,這樣方便找到緩存 key
參考資料
-
Java 演示 CPU 級的緩存效果 https://blog.csdn.net/JavaMonsterr/article/details/125147238
-
什麼是緩存雪崩、緩存擊穿、緩存穿透?https://zhuanlan.zhihu.com/p/346651831
-
Extension to the DDD skeleton project: caching in the service layer https://dotnetcodr.com/2014/03/24/extension-to-the-ddd-skeleton-project-caching-in-the-service-layer/
-
8.10.3 The MySQL Query Cache https://dev.mysql.com/doc/refman/5.7/en/query-cache.html
-
Buffer cache: What is it and how does it impact database performance? https://blog.quest.com/buffer-cache-what-is-it-and-how-does-it-impact-database-performance/
-
Webinar 筆記 http://shaogefenhao.com/libs/webinar-notes/java-solution-webinar-25.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3XfEQpBRaAWyV5rsZhpjkw