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);
}