Reids的BigKey和HotKey

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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章