Reids的BigKey和HotKey

Java架構師發表於2023-01-06

1.什麼是BigKey和HotKey

1.1.Big Key

Redis big key problem,實際上不是大Key問題,而是Key對應的value過大,因此嚴格來說是Big Value問題,Redis value is too large (key value is too large)。

到底多大的value會導致big key問題,並沒有統一的標準。

例如,對於String型別的value,有時候超過5M屬於big key,有時候穩妥起見,超過10K就可以算作Bigey。

Big Key會導致哪些問題呢?

1、由於value值很大,序列化和反序列化時間過長,網路時延也長,從而導致操作Big Key的時候耗時很長,降低了Redis的效能。

2、在叢集模式下無法做到負載均衡,導致負載傾斜到某個例項上,單例項的QPS會比較高,記憶體佔用比較多。

3、由於Redis是單執行緒,如果要對這個大Key進行刪除操作,被操作的例項可能會被block住,從而導致無法響應請求。

Big Key是如何產生的呢?

一般是程式設計者對於資料的規模預料不當,或設計考慮遺漏導致的Big Key的產生。

在某些業務場景下,很容易產生Big Key,例如KOL或者流量明星的粉絲列表、投票的統計資訊、大批次資料的快取,等等。

1.2.Hot Key

Hot Key,也叫Hotspot Key,即熱點Key。如果某個特定Key突然有大量請求,流量集中到某個例項,甚至導致這臺Redis伺服器因為達到物理網路卡上線而當機,這個時候其實就是遇到了熱點Key 問題。

熱點key會導致很多系統問題:

1、流量過度集中,無法發揮叢集優勢,如果達到該例項處理上限,導致節點當機,進而衝擊資料庫,有導致快取雪崩,讓整個系統掛掉的風險。

2、由於Redis是單執行緒操作,熱點Key會影響所在示例其他Key的操作。

2.如何發現BigKey和HotKey

2.1.發現BigKey

1、透過Redis命令查詢BigKey。

以下命令可以掃描Redis的整個Key空間不同資料型別中最大的Key。-i 0.1 引數可以在掃描的時候每100次命令執行sleep 0.1 秒。

Redis自帶的bigkeys的命令可以很方便的線上掃描大key,對服務的效能影響很小,單缺點是資訊較少,只有每個型別最大的Key。

$ redis-cli -p 999 --bigkeys -i 0.1

2、透過開源工具查詢BigKey。

使用開源工具,優點在於獲取的key資訊詳細、可選引數多、支援定製化需求,後續處理方便,缺點是需要離線操作,獲取結果時間較長。

比如,redis-rdb-tools 等等。

$ git clone https://github.com/sripathikrishnan/redis-rdb-tools 
$ cd redis-rdb-tools
$ sudo python setup.py install 
$ rdb -c memory dump-10030.rdb > memory.csv

2.2.發現HotKey

1、hotkeys 引數

Redis 在 4.0.3 版本中新增了 hotkeys (github.com/redis/redis…)查詢特性,可以直接利用 redis-cli --hotkeys 獲取當前 keyspace 的熱點 key,實現上是透過 scan + object freq 完成的。

2、monitor 命令

monitor 命令可以實時抓取出 Redis 伺服器接收到的命令,透過 redis-cli monitor 抓取資料,同時結合一些現成的分析工具,比如 redis-faina,統計出熱 Key。

3.BigKey問題的解決方法

發現和解決BigKey問題,可以參考以下思路:

1、在設計程式之初,預估value的大小,在業務設計中就避免過大的value的出現。

2、透過監控的方式,儘早發現大Key。

3、如果實在無法避免大Key,那麼可以將一個Key拆分為多個部分分別儲存到不同的Key裡。

下面以List型別的value為例,演示一下拆分解決大Key問題的方法。

有一個User Id列表,有1000萬資料,如果全部儲存到一個Key下面,會非常大,可以透過分頁拆分的方式存取資料。

下面是存取資料的程式碼實現:

/**
 * 將使用者資料寫入Redis快取
 *
 * @param userIdList
 */
public void pushBigKey(List<Long> userIdList) {
    // 將資料1000個一頁進行拆分
    int pageSize = 1000;
    List<List<Long>> userIdLists = Lists.partition(userIdList, pageSize);

    // 遍歷所有分頁,每頁資料存到1個Key中,透過字尾index進行區分
    Long index = 0L;
    for (List<Long> userIdListPart : userIdLists) {
        String pageDataKey = "user:ids:data:" + (index++);
        // 使用管道pipeline,減少獲取連線次數
        redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            for (Long userId : userIdListPart) {
                connection.lPush(pageDataKey.getBytes(), userId.toString().getBytes());
            }
            return null;
        });
        redisTemplate.expire(pageDataKey, 1, TimeUnit.DAYS);
    }

    // 存完資料,將資料的頁數存到一個單獨的Key中
    String indexKey = "user:ids:index";
    redisTemplate.opsForValue().set(indexKey, index.toString());
    redisTemplate.expire(indexKey, 1, TimeUnit.DAYS);
}

/**
 * 從Redis快取讀取使用者資料
 *
 * @return
 */
public List<Long> popBigKey() {
    String indexKey = "user:ids:index";
    String indexStr = redisTemplate.opsForValue().get(indexKey);
    if (StringUtils.isEmpty(indexStr)) {
        return null;
    }

    List<Long> userIdList = new ArrayList<>();

    Long index = Long.parseLong(indexStr);
    for (Long i = 1L; i <= index; i++) {
        String pageDataKey = "user:ids:data:" + i;
        Long currentPageSize = redisTemplate.opsForList().size(pageDataKey);
        List<Object> dataListFromRedisOnePage = redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            for (int j = 0; j < currentPageSize; j++) {
                connection.rPop(pageDataKey.getBytes());
            }
            return null;
        });
        for (Object data : dataListFromRedisOnePage) {
            userIdList.add(Long.parseLong(data.toString()));
        }
    }

    return userIdList;
}

4.HotKey問題的解決方法

如果出現了HotKey,可以考慮以下解決方案:

1、使用本地快取。比如在伺服器快取需要請求的熱點資料,這樣透過伺服器叢集的負載均衡,可以避免將大流量請求到Redis。

但本地快取會引入資料一致性問題,同時浪費伺服器記憶體。

2、HotKey將複製多份,隨機打散,使用代理請求。

/**
 * 將HotKey資料複製20份儲存
 *
 * @param key
 * @param value
 */
public void setHotKey(String key, String value) {
    int copyNum = 20;
    for (int i = 1; i <= copyNum; i++) {
        String indexKey = key + ":" + i;
        redisTemplate.opsForValue().set(indexKey, value);
        redisTemplate.expire(indexKey, 1, TimeUnit.DAYS);
    }
}

/**
 * 隨機從一個複製中獲取一個資料
 *
 * @param key
 * @return
 */
public String getHotKey(String key) {
    int startInclusive = 1;
    int endExclusive = 21;
    String randomKey = key + ":" + RandomUtils.nextInt(startInclusive, endExclusive);
    return redisTemplate.opsForValue().get(randomKey);
}

相關文章