(转载)如何解决Redis大key问题,看这一篇就够了!

Redis大key的一些场景及问题

大key场景

Redis使用者应该都遇到过大key相关的场景,比如:

1、热门话题下评论、答案排序场景。

2、大V的粉丝列表。

3、使用不恰当,或者对业务预估不准确、不及时进行处理垃圾数据等。

大key问题

由于Redis主线程为单线程模型,大key也会带来一些问题,如:

1、集群模式在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS高。

2、大key相关的删除或者自动过期时,会出现qps突降或者突升的情况,极端情况下,会造成主从复制异常,Redis服务阻塞无法响应请求。大key的体积与删除耗时可参考下表:

key类型 field数量耗时

Hash100万1000ms

List100万1000ms

Set100万1000ms

Sorted Set100万1000ms

Redis 4.0之前的大key的发现与删除方法

1、redis-rdb-tools工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析,找到其中的大KEY。

2、redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。

3、自定义的扫描脚本,以Python脚本居多,方法与redis-cli --bigkeys类似。

4、debug object key命令。可以查看某个key序列化后的长度,每次只能查找单个key的信息。官方不推荐。

redis-rdb-tools工具

关于rdb工具的详细介绍请查看链接https://github.com/sripathikrishnan/redis-rdb-tools,在此只介绍内存相关的使用方法。基本的命令为 rdb -c memory dump.rdb (其中dump.rdb为Redis实例的rdb文件,可通过bgsave生成)。

输出结果如下:

database,type,key,size_in_bytes,encoding,num_elements,len_largest_element

0,hash,hello1,1050,ziplist,86,22,

0,hash,hello2,2517,ziplist,222,8,

0,hash,hello3,2523,ziplist,156,12,

0,hash,hello4,62020,hashtable,776,32,

0,hash,hello5,71420,hashtable,1168,12,

可以看到输出的信息包括数据类型,key、内存大小、编码类型等。Rdb工具优点在于获取的key信息详细、可选参数多、支持定制化需求,结果信息可选择json或csv格式,后续处理方便,其缺点是需要离线操作,获取结果时间较长。

redis-cli --bigkeys命令

Redis-cli --bigkeys是redis-cli自带的一个命令。它对整个redis进行扫描,寻找较大的key,并打印统计结果。

例如redis-cli -p 6379 --bigkeys

Scanning the entire keyspace to find biggest keys as well as

average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

per 100 SCAN commands (not usually needed).

[00.72%] Biggest hash found so far 'hello6' with 43 fields

[02.81%] Biggest string found so far 'hello7' with 31 bytes

[05.15%] Biggest string found so far 'hello8' with 32 bytes

[26.94%] Biggest hash found so far 'hello9' with 1795 fields

[32.00%] Biggest hash found so far 'hello10' with 4671 fields

[35.55%] Biggest string found so far 'hello11' with 36 bytes

-------- summary -------

Sampled 293070 keys in the keyspace!

Total key length in bytes is 8731143 (avg len 29.79)

Biggest string found 'hello11' has 36 bytes

Biggest hash found 'hello10' has 4671 fields

238027 strings with 2300436 bytes (81.22% of keys, avg size 9.66)

0 lists with 0 items (00.00% of keys, avg size 0.00)

0 sets with 0 members (00.00% of keys, avg size 0.00)

55043 hashs with 289965 fields (18.78% of keys, avg size 5.27)

0 zsets with 0 members (00.00% of keys, avg size 0.00)

我们可以看到打印结果分为两部分,扫描过程部分,只显示了扫描到当前阶段里最大的key。summary部分给出了每种数据结构中最大的Key以及统计信息。

redis-cli --bigkeys的优点是可以在线扫描,不阻塞服务;缺点是信息较少,内容不够精确。扫描结果中只有string类型是以字节长度为衡量标准的。List、set、zset等都是以元素个数作为衡量标准,元素个数多不能说明占用内存就一定多。

自定义Python扫描脚本

通过strlen、hlen、scard等命令获取字节大小或者元素个数,扫描结果比redis-cli --keys更精细,但是缺点和redis-cli --keys一样,不赘述。

总之,之前的方法要么是用时较长离线解析,或者是不够详细的抽样扫描,离理想的以内存为维度的在线扫描获取详细信息有一定距离。由于在redis4.0前,没有lazy free机制;针对扫描出来的大key,DBA只能通过hscan、sscan、zscan方式渐进删除若干个元素;但面对过期删除键的场景,这种取巧的删除就无能为力。我们只能祈祷自动清理过期key刚好在系统低峰时,降低对业务的影响。

Redis 4.0之后的大key的发现与删除方法

Redis 4.0引入了memory usage命令和lazyfree机制,不管是对大key的发现,还是解决大key删除或者过期造成的阻塞问题都有明显的提升。

下面我们从源码(摘自Redis 5.0.4版本)来理解memory usage和lazyfree的特点。

memory usage

{"memory",memoryCommand,-2,"rR",0,NULL,0,0,0,0,0}(server.c285⾏)void memoryCommand(client c) {/...//计算key大小是通过抽样部分field来估算总大小。/elseif(!strcasecmp(c->argv[1]->ptr,"usage") &&c->argc >=3) { size_t usage = objectComputeSize(dictGetVal(de),samples);/...*/ }}(object.c1299⾏)

从上述源码看到memory usage是通过调用objectComputeSize来计算key的大小。我们来看objectComputeSize函数的逻辑。

defineOBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. /size_tobjectComputeSize(robj o, size_t sample_size){/...代码对数据类型进行了分类,此处只取hash类型说明//...//循环抽样个field,累加获取抽样样本内存值,默认抽样样本为5/while((de = dictNext(di)) != NULL && samples < sample_size) { ele = dictGetKey(de); ele2 = dictGetVal(de); elesize += sdsAllocSize(ele) + sdsAllocSize(ele2);elesize +=sizeof(structdictEntry); samples++; } dictReleaseIterator(di);/根据上一步计算的抽样样本内存值除以样本量,再乘以总的filed个数计算总内存值/if(samples) asize += (double)elesize/samplesdictSize(d);/...*/ }(object.c779⾏)

由此,我们发现memory usage默认抽样5个field来循环累加计算整个key的内存大小,样本的数量决定了key的内存大小的准确性和计算成本,样本越大,循环次数越多,计算结果更精确,性能消耗也越多。

我们可以通过Python脚本在集群低峰时扫描Redis,用较小的代价去获取所有key的内存大小。以下为部分伪代码,可根据实际情况设置大key阈值进行预警。

forkeyinr.scan_iter(count=1000):redis-cli ='/usr/bin/redis-cli'configcmd ='%s -h %s -p %s memory usage %s'% (redis-cli, rip,rport,key) keymemory = commands.getoutput(configcmd)

lazyfree机制

Lazyfree的原理是在删除的时候只进行逻辑删除,把key释放操作放在bio(Background I/O)单独的子线程处理中,减少删除大key对redis主线程的阻塞,有效地避免因删除大key带来的性能问题。在此提一下bio线程,很多人把Redis通常理解为单线程内存数据库, 其实不然。Redis将最主要的网络收发和执行命令等操作都放在了主工作线程,然而除此之外还有几个bio后台线程,从源码中可以看到有处理关闭文件和刷盘的后台线程,以及Redis4.0新增加的lazyfree线程。

/* Background job opcodes /#defineBIO_LAZY_FREE 2/ Deferred objects freeing. */(bio.h38⾏)

下面我们以unlink命令为例,来理解lazyfree的实现原理。

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},(server.c137⾏)void unlinkCommand(client *c) {delGenericCommand(c,1);}(db.c490⾏)

通过这几段源码可以看出del命令和unlink命令都是调用delGenericCommand,唯一的差别在于第二个参数不一样。这个参数就是异步删除参数。

/* This command implements DEL and LAZYDEL. /void delGenericCommand(client c, intlazy) {/.../int deleted =lazy? dbAsyncDelete(c->db,c->argv[j]) :dbSyncDelete(c->db,c->argv[j]);/.../}(db.c468⾏)

可以看到delGenericCommand函数根据lazy参数来决定是同步删除还是异步删除。当执行unlink命令时,传入lazy参数值1,调用异步删除函数dbAsyncDelete。否则执行del命令传入参数值0,调用同步删除函数dbSyncDelete。我们重点来看异步删除dbAsyncDelete的实现逻辑:

defineLAZYFREE_THRESHOLD 64/定义后台删除的阈值,key的元素大于该阈值时才真正丢给后台线程去删除/intdbAsyncDelete(redisDb db, robj key){/...//lazyfreeGetFreeEffort来获取val对象所包含的元素个数/size_tfree_effort = lazyfreeGetFreeEffort(val);/* 对删除key进行判断,满足阈值条件时进行后台删除 /if(free_effort > LAZYFREE_THRESHOLD && val->refcount ==1) {atomicIncr(lazyfree_objects,1);bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);/将删除对象放入BIO_LAZY_FREE后台线程任务队列/dictSetVal(db->dict,de,NULL);/将第一步获取到的val值设置为null/ }/...*/}(lazyfree.c53⾏)

上面提到了当删除key满足阈值条件时,会将key放入BIO_LAZY_FREE后台线程任务队列。接下来我们来看BIO_LAZY_FREE后台线程。

/.../elseif(type == BIO_LAZY_FREE) {if(job->arg1)/* 后台删除对象函数,调用decrRefCount减少key的引用计数,引用计数为0时会真正的释放资源 / lazyfreeFreeObjectFromBioThread(job->arg1);elseif(job->arg2 && job->arg3)/ 后台清空数据库字典,调用dictRelease循环遍历数据库字典删除所有key / lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);elseif(job->arg3)/ 后台删除key-slots映射表,在Redis集群模式下会用*/ lazyfreeFreeSlotsMapFromBioThread(job->arg3);}(bio.c197⾏)

unlink命令的逻辑可以总结为:执行unlink调用delGenericCommand函数传入lazy参数值1,来调用异步删除函数dbAsyncDelete,将满足阈值的大key放入BIO_LAZY_FREE后台线程任务队列进行异步删除。类似的后台删除命令还有flushdb async、flushall async。它们的原理都是获取删除标识进行判断,然后调用异步删除函数emptyDbAsnyc来清空数据库。这些命令具体的实现逻辑可自行查看flushdbCommand部分源码,在此不做赘述。

除了主动的大key删除和数据库清空操作外,过期key驱逐引发的删除操作也会阻塞Redis服务。因此Redis4.0除了增加上述三个后台删除的命令外,还增加了4个后台删除配置项,分别为slave-lazy-flush、lazyfree-lazy-eviction、lazyfree-lazy-expire和lazyfree-lazy-server-del。

slave-lazy-flush:slave接收完RDB文件后清空数据选项。建议大家开启slave-lazy-flush,这样可减少slave节点flush操作时间,从而降低主从全量同步耗时的可能性。

lazyfree-lazy-eviction:内存用满逐出选项。若开启此选项可能导致淘汰key的内存释放不够及时,内存超用。

lazyfree-lazy-expire:过期key删除选项。建议开启。

lazyfree-lazy-server-del:内部删除选项,比如rename命令将oldkey修改为一个已存在的newkey时,会先将newkey删除掉。如果newkey是一个大key,可能会引起阻塞删除。建议开启。

上述四个后台删除相关的参数实现逻辑差异不大,都是通过参数选项进行判断,从而选择是否采用dbAsyncDelete或者emptyDbAsync进行异步删除。

总结

在某些业务场景下,Redis大key的问题是难以避免的,但是,memory usage命令和lazyfree机制分别提供了内存维度的抽样算法和异步删除优化功能,这些特性有助于我们在实际业务中更好的预防大key的产生和解决大key造成的阻塞。关于Redis内核的优化思路也可从Redis作者Antirez的博客中窥测一二,他提出"Lazy Redis is better Redis"、"Slow commands threading"(允许在不同的线程中执行慢操作命令),异步化应该是Redis优化的主要方向。

Redis作为个推消息推送的一项重要的基础服务,性能的好坏至关重要。个推将Redis版本从2.8升级到5.0后,有效地解决了部分大key删除或过期造成的阻塞问题。未来,个推将会持续关注Redis 5.0及后续的Redis 6.0,与大家共同探讨如何更好地使用Redis。

参考文档:

1、http://antirez.com/news/93

2、http://antirez.com/news/126

作者:个推大数据
链接:https://www.jianshu.com/p/50c0894c0a19
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • NOSQL类型简介键值对:会使用到一个哈希表,表中有一个特定的键和一个指针指向特定的数据,如redis,volde...
    MicoCube阅读 3,958评论 2 27
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 17,030评论 1 51
  • 企业级redis集群架构的特点 海量数据 高并发 高可用 要达到高可用,持久化是不可减少的,持久化主要是做灾难恢复...
    lucode阅读 2,192评论 0 7
  • 1 Redis介绍1.1 什么是NoSql为了解决高并发、高可扩展、高可用、大数据存储问题而产生的数据库解决方...
    克鲁德李阅读 5,265评论 0 36
  • 记得三、四年前,曾经一度我十分害怕当众说话。记得经常去参加一个组织学习,主持人要求每个人要有分享。为了害怕分享。我...
    茶话心语阅读 417评论 0 0