介紹
緩存帶來了加速讀寫,降低後端負載的好處外,同時也存在一定的成本,比如資料不一緻,緩存層和資料層有時間視窗不一緻,和更新政策有關;代碼維護成本多了一層緩存邏輯;以及運維成本,例如Redis Cluster等。是以在實際的使用中,我們需要區分場景合理使用緩存邏輯。同時緩存對粒度控制分緩存全部資料和部分重要資料:
- 通用性:全量屬性更好
- 占用空間:部分屬性更好
- 代碼維護上:表面上全量屬性更好
一、緩存适用場景
緩存的适用場景示例:
- 對高消耗的SQL:join結果集/分組統計結果緩存
- 加速請求響應:利用Redis/Memcache優化IO響應時間
- 大量寫合并為批量寫:如計數器先Redis累加再批量寫DB
二、緩存更新政策
緩存的更新政策:
- 控制最大記憶體情況下,LRU/LFU/FIFO算法剔除:例如maxmemory-policy
- 逾時剔除:例如expire
- 主動更新:開發控制生命周期
三種緩存更新政策對比:
政策 | 一緻性 | 維護成本 |
LRU/LIRS算法剔除 | 最差 | 低 |
逾時剔除 | 較差 | 低 |
主動更新 | 強 | 高 |
使用建議:
- 低一緻性:最大記憶體和淘汰政策
- 高一緻性:逾時剔除和主動更新結合,最打記憶體和淘汰政策兜底
除了緩存伺服器自帶的緩存失效政策之外,我們還可以根據具體的業務需求進行自定義的緩存淘汰,常見的政策有兩種:
- 定時去清理過期的緩存
- 當有使用者請求過來時,再判斷這個請求所用到的緩存是否過期,過期的話就去底層系統得到新資料并更新緩存
兩者各有優劣,第一種的缺點是維護大量緩存的key是比較麻煩的,第二種的缺點就是每次使用者請求過來都要判斷緩存失效,邏輯相對比較複雜。
二、緩存穿透優化緩存穿透最常見的場景就是通路根本就不存在的資料。一般是黑客故意去請求緩存中不存在的資料,導緻所有的請求都落到資料庫上,造成資料庫短時間内承受大量請求而崩掉。
原因:
- 業務代碼自身問題,空變量
- 惡意攻擊、爬蟲等
解決:
1. 緩存空對象+過期時間
存在的問題:
- 需要更多的鍵
- 緩存層和存儲層資料短期不一緻
示例代碼:
public String getPassThrough(String key) { String cacheValue = cache.get(key); if (StringUtils.isBlank(cacheValue)) { String storageValue = storage.get(key); cache.set(key, storageValue); //如果存儲資料為空,需要設定一個過期時間(300秒) if (StringUtils.isBlack(storageValue)) { cache.expire(key, 60 * 5); } return storageValue; } else { return cacheValue; }}
2. 布隆過濾器攔截
最常見的則是采用布隆過濾器,将所有可能存在的資料哈希到一個足夠大的bitmap中,一個一定不存在的資料會被 這個bitmap攔截掉,進而避免了對底層存儲系統的查詢壓力。
比如10億電話本判斷電話在不在電話本中,使用很少的記憶體解決這個問題。在cache層之前增加了布隆過濾器,如果布隆過濾器過濾掉了則說明這個key是無效的,直接傳回,如果沒被過濾,則從cache層去拿資料。
三、緩存無底洞問題優化
有這麼一個場景,已經存在了很多Redis或者Memcache服務節點,發現加機器性能沒提示反而下降:http://highscalability.com/blog/2009/10/26/facebooks-memcached-multiget-hole-more-machines-more-capacit.html
問題關鍵點:
- 更多的機器!=更高的性能
- 批量接口需求(mget、mset等)
- 資料增長與水準擴充需求
是以原因就是批量操作的變化,當隻有一個節點是,一個mget操作是有一次網絡IO,當階段擴大到3個時候,使用順序IO方式的話,一次mget的操作會随着機器節點的個數增加而網絡傳輸次數也越來越多,對用戶端執行效率帶來很大的下降。實際上IO由于擴容從原來的o(1)增加到了o(node)。
優化IO的幾種方法:
- 指令本身優化:例如慢查詢keys、hgetall bigkey
- 減少網絡通信次數
- 降低接入成本:例如用戶端長連結/連接配接池、NIO等
- 串行mget
- 串行io
- 并行io
- hash_tag
串行mget、串行io、并行io以及hash_tag介紹詳見【Redis Cluster高可用叢集模式】
四種方案優缺點對比:
方案 | 優點 | 缺點 | 網絡IO |
串行mget | 少量keys滿足需求 | 大量keys請求延遲嚴重 | o(keys) |
串行IO | 少量節點滿足需求 | 大量nodes延遲嚴重 | o(nodes) |
并行IO | 延遲取決于最慢的節點 | 逾時定位問題複雜 | o(max_slow(node)) |
hash_tag | 性能最高 | 讀寫增加tag維護成本,tag分布容易出現資料傾斜 | o(1) |
四、緩存雪崩問題優化
當流量洪峰到達時,緩存同一時間大面積的失效,是以,後面的請求都會落到資料庫上,造成資料庫短時間内承受大量請求而崩掉,就是緩存雪崩。
解決方法:
- 事前:盡量保證整個 redis 叢集的高可用性,如采用Redis Cluster架構,發現機器當機盡快補上。選擇合适的記憶體淘汰政策
- 事中:本地cache緩存 + hystrix限流&降級,避免MySQL崩掉
- 事後:利用 redis 持久化機制儲存的資料盡快恢複緩存
- 對緩存進行實時監控,當請求通路的慢速度比超過門檻值,及時報警,通過自動故障轉移,服務降級,停止部分非核心接口的通路
- 提前壓測預估系統處理能力,做好限流與服務降級
五、緩存預熱優化
緩存預熱就是系統上線後,将相關的緩存資料直接加載到緩存系統。這樣就可以避免在使用者請求的時候,先查詢資料庫,然後再将資料緩存的問題,使用者直接查詢事先被預熱的緩存資料。解決思路:
- 直接寫個緩存重新整理頁面,上線時手工操作下
- 資料量不大,可以在項目啟動的時候自動進行加載
- 定時重新整理緩存
六、熱點key重建優化
熱key重建指的是開發人員設定好的緩存過期時間過了,需要重新建構緩存。熱key說明目前可能有大量的請求,同時通路同一個key,而且這個并發量特别大,緩存失效的瞬間可能會有大量的線程來重建緩存,造成後端資料庫壓力暴增。
問題描述:熱點key+較長的重建時間。
存在問題:大量的線程都會做緩存重建和查詢資料源。
解決方法:
1. 互斥鎖(mutex key)
通過設定互斥鎖,統一時間隻允許一個請求進行熱key的重建。如基于redis的setnx指令實作
存在問題:不需要大量重建工作,但是存在大量線程等待的問題。
示例代碼:
String get(String key) { String value = redis.get(key); if (value == null) { String mutexKey = "mutex:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { value = db.get(key); redis.set(key,value); redis.delete(mutexKey); } else { //其他線程休息50毫秒後重試 Thread.sleep(50); get(key); } } return value;}
2. 永不過期
為每個value添加邏輯過期時間,發現超過邏輯過期時間後,會使用單獨的線程去建構緩存,但是存在緩存不一緻情況。示例代碼:
String get(final String key) { V v = redis.get(key); String value = v.getValue(); long logicTimeout = v.getLogicTimeout(); if (logicTimeout >= System.currentTimeMills()) { String mutexKey = "mutex:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { //異步更新背景異步執行 threadPool.execute(() -> { String dbValue = db.get(key); redis.set(key,dbValue, newLogicTimeout()); redis.delete(mutexKey); }); } } return value;}
3. 方案對比
方案 | 優點 | 缺點 |
互斥鎖 | 保證一緻性 | 代碼複雜,存在死鎖風險 |
永遠不過期 | 基本杜絕熱點key重建問題 | 不保證一緻性,邏輯過期時間增加維護成本和記憶體成本 |
4. 緩存降級
與熱點key相對立的政策就是緩存降級了,服務降級的目的,是為了防止Redis服務故障,導緻資料庫跟着一起發生雪崩問題。是以,對于不重要的緩存資料,可以采取服務降級政策,例如一個比較常見的做法就是,Redis出現問題,不去資料庫查詢,而是直接傳回預設值給使用者。
推薦閱讀
1. Redis雲平台CacheCloud:https://github.com/sohutv/cachecloud
2. Redis資料結構與内部編碼,你知道多少?
3. Redis持久化機制
4. Redis Sentinel哨兵模式
5. Redis Cluster高可用叢集模式
看完本文有收獲?請轉發分享給更多人
關注「并發程式設計之美」,一起交流Java學習心得