• 如何解决Redis Big Key问题
  • 发布于 2个月前
  • 265 热度
    0 评论
一、什么是Big Key?
通俗易懂的讲,Big Key就是某个key对应的value很大,占用的redis空间很大,本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。redis中这些Big Key对应的value值很大,在序列化/反序列化过程中花费的时间很大,因此当我们操作Big Key时,通常比较耗时,这就可能导致redis发生阻塞,从而降低redis性能。

BigKey指以Key的大小和Key中成员的数量来综合判定,用几个实际的例子对大Key的特征进行描述:
1.Key本身的数据量过大:一个String类型的Key,它的值为5MB
2.Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10000个
3.Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1000个但这些成员的Value值总大小为100MB
在实际业务中,大Key的判定仍然需要根据Redis的实际使用场景、业务场景来进行综合判断。通常都会以数据大小与成员数量来判定。

二、Big Key产生的场景
1、redis数据结构使用不恰当
将Redis用在并不适合其能力的场景,造成Key的value过大,如使用String类型的Key存放大体积二进制文件型数据。

2、未及时清理垃圾数据
没有对无效数据进行定期清理,造成如HASH类型Key中的成员持续不断的增加。即一直往value塞数据,却没有删除机制,value只会越来越大。

3、对业务预估不准确
业务上线前规划设计考虑不足没有对Key中的成员进行合理的拆分,造成个别Key中的成员数量过多。

4、明星、网红的粉丝列表、某条热点新闻的评论列表
假设我们使用List数据结构保存某个明星/网红的粉丝,或者保存热点新闻的评论列表,因为粉丝数量巨大,热点新闻因为点击率、评论数会很多,这样List集合中存放的元素就会很多,可能导致value过大,进而产生Big Key问题。

三、Big Key的危害?
1、阻塞请求
Big Key对应的value较大,我们对其进行读写的时候,需要耗费较长的时间,这样就可能阻塞后续的请求处理。Redis的核心线程是单线程,单线程中请求任务的处理是串行的,前面的任务完不成,后面的任务就处理不了。

2、内存增大
读取Big Key耗费的内存比正常Key会有所增大,如果不断变大,可能会引发OOM(内存溢出),或达到redis的最大内存maxmemory设置值引发写阻塞或重要Key被逐出。

3、阻塞网络
读取单value较大时会占用服务器网卡较多带宽,自身变慢的同时可能会影响该服务器上的其他Redis实例或者应用。

4、影响主从同步、主从切换
删除一个大Key造成主库较长时间的阻塞并引发同步中断或主从切换。

四、如何识别Big Key?
方法1、使用redis-cli 命令加上--bigkeys参数 识别
可以使用Redis官方客户端redis-cli加上--bigkeys参数,可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。
优点:
可以在线扫描,不阻塞服务;
缺点
是信息较少,内容不够精确。

方法2、scan 扫描+ 长度命令
redis 老的版本,在没有scan之前,使用 key 进行扫描,新的redis版本,有了性能更好的 scan命令,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断kev的长度,推荐scan 扫描,并且,我强烈建议大家,一定实操一下。

方法3、使用debug object key命令
根据传入的对象(Key的名称)来对Key进行分析并返回大量数据,其中serializedlength的值为该Key的序列化长度,需要注意的是,Key的序列化长度并不等同于它在内存空间中的真实长度,此外,debug object属于调试命令,运行代价较大,此命令是阻塞式的,在其运行时,进入Redis的其余请求将会被阻塞直到其执行完毕。

并且每次只能查找单个key的信息,官方不推荐使用。

方法4、redis-rdb-tools开源工具
这种方式是在redis实例上执行bgsave,bgsave会触发redis的快照备份,生成rdb持久化文件,然后对dump出来的rdb文件进行分析,找到其中的大key。
GitHub地址:https://github.com/sripathikrishnan/redis-rdb-tools

优点
1.获取的key信息详细、可选参数多、支持定制化需求
2.结果信息可选择json或csv格式,后续处理方便,

缺点
1.是需要离线操作,获取结果时间较长。

五:实操一下,使用 keys 命令进行 扫描
redis 老的版本,在没有scan之前,使用 key 进行扫描,新的redis版本,有了性能更好的 scan命令,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断kev的长度,虽然推荐scan 扫描,并且,我强烈建议大家,一定实操一下。但是 知己知彼,一定要把过时的技术,体验一下。

客户端连接 redis
docker exec -it redis-standalone  redis-cli  
auth 123456
Keys 命令的使用实操
KEYS命令使用很简单,  redis KEYS 命令基本语法如下:
KEYS PATTERN
eg ,查找以 store:  为开头的 key:

key命令的性能问题
keys * 这个命令千万别在生产环境乱用。特别是数据庞大的情况下。因为Keys会引发Redis锁,并且增加Redis的CPU占用。很多公司的运维都是禁止了这个命令的当需要扫描key,匹配出自己需要的key时,可以使用 scan 命令。

Redis字符串命令
命令 说明
SET key value 用于设定指定键的值。
GET key 用于检索指定键的值。
GETRANGE key start end 返回 key 中字符串值的子字符。
GETSET key value 将给定 key 的值设置为 value,并返回 key 的旧值。
GETBIT key offset 对 key 所存储的字符串值,获取其指定偏移量上的位(bit)。
MGET key1 [key2..] 批量获取一个或多个 key 所存储的值,减少网络耗时开销。
SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
SETEX key seconds value 将值 value 存储到 key中 ,并将 key 的过期时间设为 seconds (以秒为单位)。
SETNX key value 当 key 不存在时设置 key 的值。
SETRANGE key offset value 从偏移量 offset 开始,使用指定的 value 覆盖的 key 所存储的部分字符串值。
STRLEN key 返回 key 所储存的字符串值的长度。
MSET key value [key value ...] 该命令允许同时设置多个键值对。
MSETNX key value [key value ...] 当指定的 key 都不存在时,用于设置多个键值对。
PSETEX key milliseconds value 此命令用于设置 key 的值和有过期时间(以毫秒为单位)。
INCR key 将 key 所存储的整数值加 1。
INCRBY key increment 将 key 所储存的值加上给定的递增值(increment)。
INCRBYFLOAT key increment 将 key 所储存的值加上指定的浮点递增值(increment)。
DECR key 将 key 所存储的整数值减 1。
DECRBY key decrement 将 key 所储存的值减去给定的递减值(decrement)。
APPEND key value 该命令将 value 追加到 key 所存储值的末尾。
Redis string 的命令只能一次设置/查询一个键值对,这样虽然简单,但是效率不高。为了提高命令的执行效率,Redis 提供了可以批量操作多个字符串的读写命令 MSET/MGET(“M”代表“Many”),它们允许你一次性设置或查询多个键值对,这样就有效地减少了网络耗时。

嘚瑟一下底层知识:Redis字符串的内部大小
Redis 使用标准 C 语言编写,但在存储字符时,Redis 并未使用 C 语言的字符类型,为了存储字符串,Redis 自定义了一个属于特殊结构 SDS(Simple Dynamic  String)即简单动态字符串),SDS 是一个可以修改的内部结构,非常类似于 Java 的 ArrayList。

1. SDS动态字符串
SDS 的结构定义如下:
struct sdshdr{
     //堆代码 duidaima.com
     //记录buf数组中已使用字符的数量,等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用的字符数量
     int free;
     //字符数组,用于保存字符串
     char buf[];
从上述结构体可以看出,Redis string 将字符串存储到字符类型的buf[] 、len、free

2. 分配冗余空间
string 采用了预先分配冗余空间的方式来减少内存的频繁分配,如下图所示:

如图  所示,Redis 每次给 string 分配的空间都要大于字符串实际占用的空间,这样就在一定程度上提升了 Redis string 存储的效率,比如当字符串长度变大时,无需再重新申请内存空间。

当字符串所占空间小于 1MB 时,Redis 对字符串存储空间的扩容是以成倍的方式增加的;而当所占空间超过 1MB 时,每次扩容只增加 1MB。Redis 字符串允许的最大值字节数是 512 MB。

六:实操一下,使用SCAN命令进行 扫描
scan命令和keys命令的对比
在巨大的数据量的状况下,作查找符合某种规则的Key的信息,这里就有两种方式:java
1. keys命令:
简单粗暴,可是因为Redis是单线程,keys命令是以阻塞的方式执行的,keys是以遍历的方式实现的复杂度是 O(n),Redis库中的key越多,查找实现代价越大,产生的阻塞时间越长。
2. scan命令:
以非阻塞的方式实现key值的查找,绝大多数状况下是能够替代keys命令的,可选性更强

基于SCAN的这种安全性,建议大家在生产环境都使用SCAN命令来代替KEYS,不过注意,该命令是在2.8.0版本之后加入的,如果你的Redis低于这个版本,则需要升级Redis。

1. scan相关命令
都是用于增量迭代集合元素。正则表达式。
SCAN 命令用于迭代当前数据库中的数据库键。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。
以后的例子会以sscan为例redis

2. 命令参数
SCAN  每次执行都只会返回少量元素,所以可以用于生产环境,而不会出现像 KEYS 或者 SMEMBERS 命令带来的可能会阻塞服务器的问题。

SCAN命令是一个基于游标的迭代器。redis Scan 命令基本语法如下:
SCAN cursor [MATCH pattern] [COUNT count]
cursor - 游标。
pattern - 匹配的模式。
count - 可选,用于指定每次迭代返回的 key 的数量,默认值为 10 。
pattern 参数进行样式的模糊匹配,是一个 glob 风格的模式参数,让命令只返回和给定模式相匹配的元素。比如
SCAN 0  match  store:*  count 2
每次被调用scan,  都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程;当SCAN命令的游标参数(即cursor)被设置为 0 时,redis将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

redis-cli 的使用SCAN演示:
现在有 7  个key,使用scan 扫描,每次 2个

扫描过程如下, 注意游标的 编号,不是有序的

在上面这个例子中, 第一次迭代使用 0 作为游标, 表示开始一次新的迭代。

第二次迭代使用的是第一次迭代时返回的游标8,作为新的迭代参数 。显而易见,SCAN命令的返回值, 是一个包含两个元素的数组:第一个数组元素是用于进行下一次迭代的新游标,而第二个数组元素则又是一个数组, 这个数组中包含了所有被迭代的元素。
一次scan full iteration(完全迭代) 的过程如下:
以 0 作为游标开始一次新的迭代, 一直调用 SCAN 命令, 直到命令返回游标 0 , 我们称这个过程为一次完整遍历。

注意两点:
1.返回的游标不一定是递增的,可能后一次返回的游标比前一次的小。
2.SCAN增量式迭代命令并不保证每次执行都返回某个给定数量的元素,甚至可能会返回零个元素, 但只要命令返回的游标不是 0 , 应用程序就不应该将迭代视作结束。
在最后一次调用 SCAN 命令时, 命令返回了游标 0 , 这表示迭代已经结束, 整个数据集已经被完整遍历过了。

七、SpringBoot BigKey的scan扫描实操
SpringBoot  应用中,可是经过用scan,咱们就能够指定有共性的key,并指定一次性查询条件。
演示的代码如下:

这里例子中,是 以 大于 50个字节,就计算为 bigkey.这个阈值,仅仅是为了演示方便,生产场景,可以设置一个大的阈值,比如,一个String类型的Key,它的阈值为5MB

执行的结果
启动应用,可以得到执行的结果

实验完美成功

生产场景的bigkey 扫描
结合scan + 定时任务的方式,  在 吞吐量的低峰期,进行扫描。发现了bigkey, 可以及时的进行 运维 告警, 发送  邮件通知或者    钉钉企业信息。

类似场景,对大量key进行扫描的集群
在线上有时候,须要对大量key进行扫描(如删除)操做,有几个风险点:
1.一次性查询所指定的key
如果是使用keys,数量较大可能形成redis服务卡顿,Redis是单线程程序,顺序执行全部指令,其它指令必须等到当前的 keys 指令执行完了才能够继续。

2.从海量的 key 中找出知足特定前缀的 key
上面的场景中,都可以用scan,咱们就能够指定有共性的key,并指定一次性查询条件。

要点是:使用SCAN命令扫描key替代KEYS避免redis服务器阻塞,无坑!

八、如何解决Big Key问题?
要解决Big Key问题,无非就是减小key对应的value值的大小,也就是对于String数据结构的话,减少存储的字符串的长度;对于List、Hash、Set、ZSet数据结构则是减少集合中元素的个数。

1、对大Key进行拆分
将一个Big Key拆分为多个key-value这样的小Key,并确保每个key的成员数量或者大小在合理范围内,然后再进行存储,通过get不同的key或者使用mget批量获取。

2、对大Key进行清理
对Redis中的大Key进行清理,从Redis中删除此类数据。

Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,

通过UNLINK,你可以安全的删除大Key甚至特大Key。

3、监控Redis的内存、网络带宽、超时等指标
通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。

4、定期清理失效数据
如果某个Key有业务不断以增量方式写入大量的数据,并且忽略了其时效性,这样会导致大量的失效数据堆积。

可以通过定时任务的方式,对失效数据进行清理。

5、压缩value
使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。

如果压缩后,value还是很大,那么可以进一步对key进行拆分。
用户评论