Redis 性能調優——緩存設計優化

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 單線程架構爲什麼這麼快?

  1. 主要原因:純內存;
  2. 非阻塞 IO,Redis 使用 Event Loop 這樣的模型作爲 IO 多路複用的實現,並且 Redis 自身實現了一個事件處理,將 Event Loop 連接、讀寫、關閉轉換爲自身的一個事件,不再往 IO 上浪費過多時間;
  3. 避免線程切換和競態消耗;

單線程架構要注意什麼?

  1. 一次只運行一條命令;
  2. 拒絕長(慢)命令,例如 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 問題,開發人員在使用時要了解它們各自的使用成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章