JAVA秘籍之Redis BigKey

一账胧、什么是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源碼、筆記豆赏、資料哦挣菲!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末富稻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子己单,更是在濱河造成了極大的恐慌唉窃,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纹笼,死亡現(xiàn)場(chǎng)離奇詭異纹份,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)廷痘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門蔓涧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人笋额,你說我怎么就攤上這事元暴。” “怎么了兄猩?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵茉盏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我枢冤,道長(zhǎng)鸠姨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任淹真,我火速辦了婚禮讶迁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘核蘸。我一直安慰自己巍糯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布客扎。 她就那樣靜靜地躺著祟峦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪徙鱼。 梳的紋絲不亂的頭發(fā)上搀愧,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音疆偿,去河邊找鬼。 笑死搓幌,一個(gè)胖子當(dāng)著我的面吹牛杆故,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溉愁,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼处铛,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼饲趋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起撤蟆,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤奕塑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后家肯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龄砰,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年讨衣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了换棚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡反镇,死狀恐怖固蚤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情歹茶,我是刑警寧澤夕玩,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站惊豺,受9級(jí)特大地震影響燎孟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扮叨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一缤弦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彻磁,春花似錦碍沐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至磁浇,卻和暖如春斋陪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背置吓。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工无虚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人衍锚。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓友题,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親戴质。 傳聞我的和親對(duì)象是個(gè)殘疾皇子度宦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359