一账胧、什么是bigkey
在Redis中,一個(gè)字符串最大512MB落蝙,一個(gè)二級(jí)數(shù)據(jù)結(jié)構(gòu)(例如hash织狐、list暂幼、set、zset)可以存儲(chǔ)大約40億個(gè)(2^32-1)個(gè)元素移迫,但實(shí)際上中如果下面兩種情況旺嬉,我就會(huì)認(rèn)為它是bigkey。
字符串類型:它的big體現(xiàn)在單個(gè)value值很大厨埋,一般認(rèn)為超過10KB就是bigkey邪媳。
非字符串類型:哈希、列表荡陷、集合雨效、有序集合,它們的big體現(xiàn)在元素個(gè)數(shù)太多废赞。
二徽龟、危害
bigkey可以說就是Redis的老鼠屎,具體表現(xiàn)在:
1.內(nèi)存空間不均勻
這樣會(huì)不利于集群對(duì)內(nèi)存的統(tǒng)一管理唉地,存在丟失數(shù)據(jù)的隱患据悔。
2.超時(shí)阻塞
由于Redis單線程的特性,操作bigkey的通常比較耗時(shí)耘沼,也就意味著阻塞Redis可能性越大极颓,這樣會(huì)造成客戶端阻塞或者引起故障切換,它們通常出現(xiàn)在慢查詢中耕拷。
例如讼昆,在Redis發(fā)現(xiàn)了這樣的key,你就等著DBA找你吧骚烧。
127.0.0.1:6379>hlenbig:hash(integer)
2000000127.0.0.1:6379>hgetallbig:hash
1)"a"
2) "1"
3.網(wǎng)絡(luò)擁塞
bigkey也就意味著每次獲取要產(chǎn)生的網(wǎng)絡(luò)流量較大浸赫,假設(shè)一個(gè)bigkey為1MB,客戶端每秒訪問量為1000赃绊,那么每秒產(chǎn)生1000MB的流量既峡,對(duì)于普通的千兆網(wǎng)卡(按照字節(jié)算是128MB/s)的服務(wù)器來說簡(jiǎn)直是滅頂之災(zāi),而且一般服務(wù)器會(huì)采用單機(jī)多實(shí)例的方式來部署碧查,也就是說一個(gè)bigkey可能會(huì)對(duì)其他實(shí)例造成影響运敢,其后果不堪設(shè)想。
4.過期刪除
有個(gè)bigkey忠售,它安分守己(只執(zhí)行簡(jiǎn)單的命令传惠,例如hget、lpop稻扬、zscore等)卦方,但它設(shè)置了過期時(shí)間,當(dāng)它過期后泰佳,會(huì)被刪除盼砍,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazy-expire yes)尘吗,就會(huì)存在阻塞Redis的可能性,而且這個(gè)過期刪除不會(huì)從主節(jié)點(diǎn)的慢查詢發(fā)現(xiàn)(因?yàn)檫@個(gè)刪除不是客戶端產(chǎn)生的浇坐,是內(nèi)部循環(huán)事件睬捶,可以從latency命令中獲取或者從slave節(jié)點(diǎn)慢查詢發(fā)現(xiàn))。
5.遷移困難
當(dāng)需要對(duì)bigkey進(jìn)行遷移(例如Redis cluster的遷移slot)近刘,實(shí)際上是通過migrate命令來完成的擒贸,migrate實(shí)際上是通過dump + restore + del三個(gè)命令組合成原子命令完成,如果是bigkey跌宛,可能會(huì)使遷移失敗酗宋,而且較慢的migrate會(huì)阻塞Redis积仗。
三疆拘、怎么產(chǎn)生的?
一般來說寂曹,bigkey的產(chǎn)生都是由于程序設(shè)計(jì)不當(dāng)哎迄,或者對(duì)于數(shù)據(jù)規(guī)模預(yù)料不清楚造成的,來看幾個(gè):
(1)?社交類:粉絲列表隆圆,如果某些明星或者大v不精心設(shè)計(jì)下漱挚,必是bigkey。
(2)?統(tǒng)計(jì)類:例如按天存儲(chǔ)某項(xiàng)功能或者網(wǎng)站的用戶集合渺氧,除非沒幾個(gè)人用旨涝,否則必是bigkey。
(3)?緩存類:將數(shù)據(jù)從數(shù)據(jù)庫(kù)load出來序列化放到Redis里侣背,這個(gè)方式非常常用白华,但有兩個(gè)地方需要注意:
第一,是不是有必要把所有字段都緩存
第二贩耐,有沒有相關(guān)關(guān)聯(lián)的數(shù)據(jù)
例如遇到過一個(gè)例子弧腥,該同學(xué)將某明星一個(gè)專輯下所有視頻信息都緩存一個(gè)巨大的json中,造成這個(gè)json達(dá)到6MB潮太,后來這個(gè)明星發(fā)了一個(gè)官宣
四管搪、如何發(fā)現(xiàn)
1. redis-cli --bigkeys
redis-cli提供了--bigkeys來查找bigkey,例如下面就是一次執(zhí)行結(jié)果:
--------summary-------
Biggeststringfound'user:1'has5bytes
Biggestlistfound'taskflow:175448'has97478items
Biggestsetfound'redisServerSelect:set:11597'has49members
Biggesthashfound'loginUser:t:20180905'has863fields
Biggestzsetfound'hotkey:scan:instance:zset'has3431members
40stringswith200bytes(00.00%ofkeys,avgsize5.00)
2747619listswith14680289items(99.86%ofkeys,avgsize5.34)
2855setswith10305members(00.10%ofkeys,avgsize3.61)
13hashswith2433fields(00.00%ofkeys,avgsize187.15)
830zsetswith14098members(00.03%ofkeys,avgsize16.99)
可以看到--bigkeys給出了每種數(shù)據(jù)結(jié)構(gòu)的top 1 bigkey铡买,同時(shí)給出了每種數(shù)據(jù)類型的鍵值個(gè)數(shù)以及平均大小更鲁。
bigkeys對(duì)問題的排查非常方便,但是在使用它時(shí)候也有幾點(diǎn)需要注意:
建議在從節(jié)點(diǎn)執(zhí)行奇钞,因?yàn)?-bigkeys也是通過scan完成的澡为。
建議在節(jié)點(diǎn)本機(jī)執(zhí)行,這樣可以減少網(wǎng)絡(luò)開銷蛇券。
如果沒有從節(jié)點(diǎn)缀壤,可以使用--i參數(shù)樊拓,例如(--i 0.1 代表100毫秒執(zhí)行一次)
--bigkeys只能計(jì)算每種數(shù)據(jù)結(jié)構(gòu)的top1,如果有些數(shù)據(jù)結(jié)構(gòu)非常多的bigkey塘慕,也搞不定筋夏,畢竟不是自己寫的東西嘛
debug object
再來看一個(gè)場(chǎng)景:
你好,麻煩幫我查一下Redis里大于10KB的所有key
您好图呢,幫忙查一下Redis中長(zhǎng)度大于5000的hash key
是不是發(fā)現(xiàn)用--bigkeys不行了(當(dāng)然如果改源碼也不是太難)条篷,但有沒有更快捷的方法,Redis提供了debug object ${key}命令獲取鍵值的相關(guān)信息:
127.0.0.1:6379>hlenbig:hash
(integer)5000000
127.0.0.1:6379>debugobjectbig:hash
Valueat:0x7fda95b0cb20refcount:1encoding:hashtableserializedlength:87777785lru:9625559lru_seconds_idle:2
(1.08s)
其中serializedlength表示key對(duì)應(yīng)的value序列化之后的字節(jié)數(shù)蛤织,當(dāng)然如果是字符串類型赴叹,完全看可以執(zhí)行strlen,例如:
127.0.0.1:6379>strlenkey
(integer) 947394
這樣你就可以用scan + debug object的方式遍歷Redis所有的鍵值指蚜,找到你需要閾值的數(shù)據(jù)了乞巧。
但是在使用debug object時(shí)候一定要注意以下幾點(diǎn):
debug object bigkey本身可能就會(huì)比較慢,它本身就會(huì)存在阻塞Redis的可能
建議在從節(jié)點(diǎn)執(zhí)行
建議在節(jié)點(diǎn)本地執(zhí)行
如果不關(guān)系具體字節(jié)數(shù)摊鸡,完全可以使用scan + strlen|hlen|llen|scard|zcard替代免猾,他們都是o(1)
3. memory usage
上面的debug object可能會(huì)比較危險(xiǎn)猎提、而且不太準(zhǔn)確(序列化后的長(zhǎng)度),有沒有更準(zhǔn)確的呢疙教?Redis 4.0開始提供memory usage命令可以計(jì)算每個(gè)鍵值的字節(jié)數(shù)(自身松逊、以及相關(guān)指針開銷经宏,具體的細(xì)節(jié)可查閱相關(guān)文章),例如下面是一次執(zhí)行結(jié)果:
127.0.0.1:6379>memoryusagebig:hash
(integer)318663444
下面我們來對(duì)比就可以看出來沪斟,當(dāng)前系統(tǒng)就一個(gè)key择吊,總內(nèi)存消耗是400MB左右几睛,memory usage相比debug object還是要精確一些的。
127.0.0.1:6379>dbsize
(integer) 1
127.0.0.1:6379>hlenbig:hash
(integer)5000000
#約300MB
127.0.0.1:6379>memoryusagebig:hash
(integer)318663444
#約85MB
127.0.0.1:6379>debugobjectbig:hash
Valueat:0x7fda95b0cb20refcount:1encoding:hashtableserializedlength:87777785lru:9625814lru_seconds_idle:9
(1.06s)
127.0.0.1:6379>infomemory
#Memory
used_memory_human:402.16M
如果你使用Redis 4.0+,你就可以用scan + memory usage(pipeline)了晴弃,而且很好的一點(diǎn)是,memory不會(huì)執(zhí)行很慢肝匆,當(dāng)然依然是建議從節(jié)點(diǎn) + 本地 枯怖。
4. 客戶端
上面三種方式都有一個(gè)問題肿轨,就是馬后炮,如果想很實(shí)時(shí)的找到bigkey驹暑,一方面你可以試試修改Redis源碼,還有一種方式就是可以修改客戶端帆焕,以jedis為例财饥,可以在關(guān)鍵的出入口加上對(duì)應(yīng)的檢測(cè)機(jī)制,例如以Jedis的獲取結(jié)果為例子:
protectedObjectreadProtocolWithCheckingBroken(){
Object o =null;
try{
o = Protocol.read(inputStream);returno;
}catch(JedisConnectionException exc) {
UsefulDataCollector.collectException(exc, getHostPort(), System.currentTimeMillis());broken =true;
throwexc;
}finally{
if(o !=null) {
if(oinstanceofbyte[]) {
byte[] bytes = (byte[]) o;
if(bytes.length > threshold) {
// 做很多事情,例如用ELK完成收集和展示
}
}
}
}
}
5. 監(jiān)控報(bào)警
bigkey的大操作编饺,通常會(huì)引起客戶端輸入或者輸出緩沖區(qū)的異常豁鲤,Redis提供了info clients里面包含的客戶端輸入緩沖區(qū)的字節(jié)數(shù)以及輸出緩沖區(qū)的隊(duì)列長(zhǎng)度锅论,可以重點(diǎn)關(guān)注下:
如果想知道具體的客戶端,可以使用client list命令來查找
redis-cli client list
id=3addr=127.0.0.1:58500fd=8name= age=3978idle=25flags=N db=0sub=0psub=0multi=-1qbuf=0qbuf-free=0obl=0oll=0omem=26263554events=r cmd=hgetall
6. 改源碼
這個(gè)其實(shí)也是能做的,但是各方面成本比較高,對(duì)于一般公司來說不適用鄙早。
建議的最佳實(shí)踐:
Redis端與客戶端相結(jié)合:--bigkeys臨時(shí)用第美、scan長(zhǎng)期做排除隱患(盡可能本地化)蝶锋、客戶端實(shí)時(shí)監(jiān)控扳缕。
監(jiān)控報(bào)警要跟上
debug object盡量少用
所有數(shù)據(jù)平臺(tái)化
要和開發(fā)同學(xué)強(qiáng)調(diào)bigkey的危害
五躯舔、如何刪除
如果發(fā)現(xiàn)了bigkey,而且確認(rèn)是垃圾是不是直接del就可以了,來看一組數(shù)據(jù):
可以看到對(duì)于string類型坑鱼,刪除速度還是可以接受的呼股。但對(duì)于二級(jí)數(shù)據(jù)結(jié)構(gòu),隨著元素個(gè)數(shù)的增長(zhǎng)以及每個(gè)元素字節(jié)數(shù)的增大画恰,刪除速度會(huì)越來越慢马靠,存在阻塞Redis的隱患。所以在刪除它們時(shí)候建議采用漸進(jìn)式的方式來完成:hscan蔼两、ltrim、sscan逞度、zscan额划。
如果你使用Redis 4.0+,一條異步刪除unlink就解決档泽,就可以忽略下面內(nèi)容俊戳。
1. 字符串
一般來說,對(duì)于string類型使用del命令不會(huì)產(chǎn)生阻塞馆匿。
delbigkey
2. hash
使用hscan命令抑胎,每次獲取部分(例如100個(gè))field-value,在利用hdel刪除每個(gè)field(為了快速可以使用pipeline)渐北。
public void delBigHash(String bigKey) {
Jedis jedis = new Jedis("127.0.0.1",6379);
//游標(biāo)
String cursor ="0";
while(true) {
ScanResult> scanResult = jedis.hscan(bigKey, cursor, new ScanParams().count(100));
//每次掃描后獲取新的游標(biāo)
cursor = scanResult.getStringCursor();//獲取掃描結(jié)果
List> list = scanResult.getResult();if(list == null||list.size() ==0) {
continue;}String[] fields = getFieldsFrom(list);//刪除多個(gè)field
jedis.hdel(bigKey, fields);//游標(biāo)為0時(shí)停止
if(cursor.equals("0")) {
break;
}}// 最終刪除key
jedis.del(bigKey);
}
/**
* 獲取field數(shù)組 */
private String[] getFieldsFrom(List<Entry<String, String>> list) {
List<String> fields = new ArrayList<String>();
for (Entry<String, String> entry : list) {
fields.add(entry.getKey());
}
return fields.toArray(new String[fields.size()]);
}
3. list
Redis并沒有提供lscan這樣的API來遍歷列表類型阿逃,但是提供了ltrim這樣的命令可以漸進(jìn)式的刪除列表元素,直到把列表刪除。
publicvoiddelBigList(String bigKey){
Jedis jedis =newJedis("127.0.0.1",6379);
longllen = jedis.llen(bigKey);
intcounter =0;
intleft =100;
while(counter < llen) {
// 每次從左側(cè)截掉100個(gè)
jedis.ltrim(bigKey, left, llen);
counter += left;
}
// 最終刪除key
jedis.del(bigKey);
}
4. set
使用sscan命令恃锉,每次獲取部分(例如100個(gè))元素搀菩,在利用srem刪除每個(gè)元素。
public void delBigSet(String bigKey) {
Jedis jedis =newJedis("127.0.0.1",6379);
//游標(biāo)
String cursor ="0";
while(true) {
ScanResult scanResult = jedis.sscan(bigKey, cursor,newScanParams().count(100));
//每次掃描后獲取新的游標(biāo)
cursor = scanResult.getStringCursor();//獲取掃描結(jié)果
List list = scanResult.getResult();if(list ==null|| list.size() ==0) {
continue;
}jedis.srem(bigKey, list.toArray(newString[list.size()]));
//游標(biāo)為0時(shí)停止
if(cursor.equals("0")) {
break;
}}//最終刪除key
jedis.del(bigKey);}
5. sorted set
使用zscan命令破托,每次獲取部分(例如100個(gè))元素肪跋,在利用zremrangebyrank刪除元素。
public void delBigSortedSet(String bigKey) {
long startTime = System.currentTimeMillis();Jedis jedis = new Jedis(HOST, PORT);//游標(biāo)
String cursor ="0";
while(true) {
ScanResult scanResult = jedis.zscan(bigKey, cursor, new ScanParams().count(100));
//每次掃描后獲取新的游標(biāo)
cursor = scanResult.getStringCursor();//獲取掃描結(jié)果
List list = scanResult.getResult();if(list == null||list.size() ==0) {
continue;}String[] members = getMembers(list);jedis.zrem(bigKey, members);//游標(biāo)為0時(shí)停止
if(cursor.equals("0")) {
break;
}}// 最終刪除key
jedis.del(bigKey);
}
public void delBigSortedSet2(String bigKey) {
Jedis jedis = new Jedis(HOST, PORT);
long zcard = jedis.zcard(bigKey);
int counter = 0;
int incr = 100;
while(counter < zcard) {
jedis.zremrangeByRank(bigKey, 0, 100);
// 每次從左側(cè)截掉100個(gè)
counter += incr;
}
// 最終刪除key
jedis.del(bigKey);
}
六土砂、如何優(yōu)化
1.拆分
big list: list1州既、list2、...listN
big hash:可以做二次的hash萝映,例如hash%100
日期類:key20190320吴叶、key20190321、key_20190322锌俱。
2.本地緩存
減少訪問redis次數(shù)晤郑,降低危害,但是要注意這里有可能因此本地的一些開銷(例如使用堆外內(nèi)存會(huì)涉及序列化贸宏,bigkey對(duì)序列化的開銷也不性烨蕖)
7、總結(jié):
由于開發(fā)人員對(duì)Redis的理解程度不同吭练,在實(shí)際開發(fā)中出現(xiàn)bigkey在所難免诫龙,重要的能通過合理的檢測(cè)機(jī)制及時(shí)找到它們,進(jìn)行處理鲫咽。作為開發(fā)人員應(yīng)該在業(yè)務(wù)開發(fā)時(shí)不能將Redis簡(jiǎn)單暴力的使用签赃,應(yīng)該在數(shù)據(jù)結(jié)構(gòu)的選擇和設(shè)計(jì)上更加合理,例如出現(xiàn)了bigkey分尸,要思考一下可不可以做一些優(yōu)化(例如二級(jí)索引)盡量的讓這些bigkey消失在業(yè)務(wù)中锦聊,如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出來(例如有時(shí)候僅僅需要hmget箩绍,而不是hgetall)孔庭,刪除也是一樣,盡量使用優(yōu)雅的方式來處理材蛛。
?
由于篇幅限制圆到,更多的Redis介紹小編放在下面的文檔里了,需要獲取完整文檔用以學(xué)習(xí)的朋友們可以轉(zhuǎn)發(fā)+關(guān)注卑吭,私信領(lǐng)取芽淡,還有更多java源碼、筆記豆赏、資料哦挣菲!