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