Redis 是一個開源的高性能的 Key-Value 服務器。本篇主要介紹一下緩存的設計與優化。
1. 緩存的受益與成本
- | 說明 |
---|---|
緩存的受益 | 1、加速讀寫,通過緩存加速讀寫速度,例如 CPU L1/L2/L3 Cache、Linux page Cache 加速硬盤讀寫、瀏覽器緩存、Ehcache 緩存數據庫結果; 2、降低後端負載,後端服務器通過前端緩存降低負載,業務端使用 Redis 降低後端 MySQL 負載等。 |
緩存的成本 | 1、數據不一致,緩存和數據層有時間窗口不一致,和更新策略有關; 2、代碼維護成本增加,多了一層緩存邏輯; 3、運維成本增加。 |
緩存的使用場景:
- 降低後端負載,對高消耗的 SQL,例如 join 結果集/分組統計結果緩存;
- 加速請求響應,利用 Redis/Memcache 優化 IO 響應時間;
- 大量寫合併爲批量寫,例如計數器先 Redis 累加再批量寫 DB。
2.單線程架構
Redis 在一個同一時間點只會執行一條命令。
大多情況下,單線程是非常慢的。Redis 單線程架構爲什麼這麼快?
- 主要原因:純內存;
- 非阻塞 IO,Redis 使用 Event Loop 這樣的模型作爲 IO 多路複用的實現,並且 Redis 自身實現了一個事件處理,將 Event Loop 連接、讀寫、關閉轉換爲自身的一個事件,不再往 IO 上浪費過多時間;
- 避免線程切換和競態消耗;
單線程架構要注意什麼?
- 一次只運行一條命令;
- 拒絕長(慢)命令,例如 keys、flushall、flushdb、slow lua scrip、mutil/exec、operate big value(collection);
2.緩存更新策略
策略 | 說明 | 一致性 | 維護成本 |
---|---|---|---|
LRU/LFU/FIFO 算法剔除 | 例如 maxmemory-policy | 最差 | 低 |
超時剔除 | 例如 expire | 較差 | 低 |
主動更新 | 開發控制生命週期 | 強 | 高 |
兩條建議:
低一致性:推薦最大內存和淘汰策略;
高一致性:推薦超時剔除和主動更新結合,超時剔除是給主動更新做了一個兜底,還需要最大內存和淘汰策略二次兜底。
3.緩存粒度控制
從 MySQL 獲取用戶信息:select * from user where id = {id}
設置用戶信息緩存:set user:{id} ‘select * from user where id = {id}’
緩存粒度:
- 全部屬性:set user:{id} ‘select * from user where id = {id}’
- 部分重要屬性:set user:{id} ‘select importantColumn1, …importantColumnK from user where id = {id}’
緩存粒度控制的三個角度:
通用性:全部屬性更好;
佔用空間:部分重要屬性更好;
代碼維護:表面上全部屬性更好,增刪字段不需要維護代碼。
4.緩存穿透優化
緩存穿透問題,大量請求不命中?
發生緩存穿透的常見原因:
- 業務代碼自身問題;
- 惡意攻擊、爬蟲等等。
如何發現問題?
- 業務的響應時間;
- 業務本身問題;
- 相關監控指標:總調用數、緩存層命中數、存儲層命中數;
緩存穿透問題解決方案:
方案一:緩存空對象。示例代碼:
public String getPassThrough(String key) {
String cacheValue = cache.get(key);
if (StringUtils.isBlank(cacheValue)) {
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存儲數據爲空, 需要設置過期時間
if (StringUtils.isBlank(storageValue)) {
cache.expire(key, 300); // 300秒
}
return storageValue;
} else {
return cacheValue;
}
}
方案二:布隆過濾器攔截。通過很小的內存來實現對數據的過濾。
5.緩存雪崩優化
緩存雪崩:由於 cache 服務承載大量請求,當 cache 服務異常/脫機後,流量直接壓向後端組件(例如 DB),造成級聯故障。
緩存雪崩優化方案:
- 保證緩存高可用性,例如 Redis Cluster、Redis Sentinel、VIP;
- 依賴隔離組件爲後端限流;
- 提前演練,例如壓力測試。
6.無底洞問題優化
無底洞問題:增加機器性能沒能提升,反而下降。問題關鍵點就是批量操作的鏈化,例如 mget 操作,時間複雜度爲 O(node),隨着機器的增加,mget 批量操作的時間會越長,更多的機器不代表更多的性能。
但是隨着數據增長,水平擴展是必須的。
優化 IO 的幾種方法:
- 命令本身優化,例如慢查詢 keys、hgetall bigkey;
- 減少網絡通信次數;
- 降低接入成本,例如客戶端使用長連接/連接池、NIO 等 。
7.熱點key優化
發現熱點key:
方法一:客戶端,可以使用 Guava 的 AtomicLongMap,記錄 key 的調用次數:
public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
String get(String key) {
counterKey(key);
...
}
String set(String key, String value) {
counterKey(key);
...
}
方法二:代理端
客戶端和 Redis 中間加一個代理進行收集統計。
方法三:服務端
使用 monitor 解析,輸出統計。
方法四:機器收集
抓取分析 Redis 所在機器的 TPC 數據。
四種方式對比:
方案 | 優點 | 缺點 |
---|---|---|
客戶端 | 1、實現簡單; | 1、內存泄露隱患,如果 key 量太大不建議使用; 2、維護成本高; 3、只能統計單個客戶端; |
代理端 | 1、代理是客戶端和服務端的橋樑,實現最方便最系統; | 1、增加代理端的開發部署成本; |
服務端 | 1、實現簡單; | 1、monitor 本身的使用成本和危害,只能短時間使用; 2、只能統計單個 Redis 節點; |
機器收集 | 1、對於客戶端和服務端無侵入和影響; | 1、需要專業的運維團隊開發,並且增加了機器的部署成本; |
優化方案:
- 避免 bigkey;
- 熱鍵不要用 hash_tag,因爲 hash_tag 會落到一個節點上;
- 如果真有熱點 key 而且業務對一致性要求不高時,可以用本地緩存 + MQ 解決。
8.熱點key重建優化
問題:熱點 key + 較長的重建時間。
獲取緩存 -> 查詢數據源 -> 重建緩存 -> 輸出,這個步驟在高併發的情況下,由於查詢數據源需要時間,所以會有很多請求會進入到 查詢數據源 -> 重建緩存 這個過程。對數據源會造成很大壓力,響應時間也會變慢。
三個優化目標:
- 減少重建緩存的次數;
- 數據儘可能一致;
- 減少潛在風險。
兩個優化方案:
- 互斥鎖(mutex key),查詢數據源 -> 重建緩存 這個過程加互斥鎖;
- 永不過期,緩存層面不設置過期時間(沒有用 expire),功能層面爲每個 value 添加邏輯過期時間,但發現超過邏輯過期時間後,會使用單獨的線程去構建緩存。
兩個優化方案的對比:
策略 | 優點 | 缺點 |
---|---|---|
互斥鎖 | 思路簡單,保證一致性 | 代碼複雜度增加,存在死鎖的風險 |
永不過期 | 基本杜絕熱點 key 重建問題 | 不保證一致性,邏輯過期時間增加維護成本和內存成本 |
9.總結
- 緩存收益:加速讀寫、降低後端存儲負載;
- 緩存成本:緩存和存儲數據不一致性、代碼維護成本、運維成本;
- 推薦結合剔除、超時、主動更新三種方案共同完成;
- 穿透問題:使用緩存空對象和布隆過濾器來解決,注意它們各自的使用場景和侷限性;
- 無底洞問題:分佈式緩存中,有更多的機器不保證有更高的性能。有四種批量操作方式:串行命令、串行 IO、並行 IO、hash_tag;
- 雪崩問題:緩存層高可用、客戶端降級、提前演練是解決雪崩問題的重要方法;
- 熱點 key 重建問題:互斥鎖、永不過期能夠在一定程度上解決熱點 key 問題,開發人員在使用時要了解它們各自的使用成本。