redis之bigkey(看这一篇就够)

bigkey

1、bigkey带来的问题

  1. 如果是集群模式下,无法做到负载均衡,导致请求倾斜到某个实例上,而这个实例的QPS会比较大,内存占用也较多;对于Redis单线程模型又容易出现CPU瓶颈,当内存出现瓶颈时,只能进行纵向库容,使用更牛逼的服务器。
  2. 涉及到大key的操作,尤其是使用hgetall、lrange 0 -1、get、hmget 等操作时,网卡可能会成为瓶颈,也会到导致堵塞其它操作,qps 就有可能出现突降或者突升的情况,趋势上看起来十分不平滑,严重时会导致应用程序连不上,实例或者集群在某些时间段内不可用的状态。
  3. 假如这个key需要进行删除操作,如果直接进行DEL 操作,被操作的实例会被Block住,导致无法响应应用的请求,而这个Block的时间会随着key的变大而变长。

2、bigkey是如何产生的

一般来说,bigkey是由于程序员的程序设计不当,或对数据规模预料不清楚造成的:
1、社交类:粉丝列表,如果某些明显或大V,一定是bigkey
2、统计类:如果按天存储某项功能或网站的用户集合,除非没几个人用,否则必定是bigkey
3、缓存类:作为数据库数据的冗余存储,这种是redis的最常用场景,但有2点要注意:
1)是不是有必要把所有数据都缓存
2)有没有相关关联的数据
举个例子,该同学把某明星一个专辑下的所有视频信息都缓存成了一个巨大的json,这个json达到了6MB。

3、查找bigKey的方法

  1. 在redis实例上执行bgsave,然后我们对dump出来的rdb文件进行分析,找到其中的大KEY
  2. 有个不太推荐的命令,debug object xxx 可以看到这个key在内存中序列化后的大小,当然我们可以通过SCAN+debug object xxx 得到当前实例所有key的大小。
  3. redis-cli 原生自带 –bigkeys 功能,可以找到某个实例 5种数据类型(String、hash、list、set、zset)的最大key。

4、直接删除bigkey的风险

DEL命令在删除单个集合类型的Key时,命令的时间复杂度是O(M),其中M是集合类型Key包含的元素个数。

DEL keyTime complexity: O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).

生产环境中遇到过多次因业务删除大Key,导致Redis阻塞,出现故障切换和应用程序雪崩的故障。测试删除集合类型大Key耗时,一般每秒可清理100w~数百w个元素; 如果数千w个元素的大Key时,会导致Redis阻塞上10秒可能导致集群判断Redis已经故障,出现故障切换;或应用程序出现雪崩的情况。

说明:Redis是单线程处理。单个耗时过大命令,导致阻塞其他命令,容易引起应用程序雪崩或Redis集群发生故障切换。所以避免在生产环境中使用耗时过大命令。

Redis删除大的集合键的耗时, 测试估算,可参考;和硬件环境、Redis版本和负载等因素有关

Key类型 Item数量 耗时
Hash ~100万 ~1000ms
List ~100万 ~1000ms
Set ~100万 ~1000ms
Sorted Set ~100万 ~1000ms

5、如何优雅地删除各类大Key

从Redis2.8版本开始支持SCAN命令,通过m次时间复杂度为O(1)的方式,遍历包含n个元素的大key.这样避免单个O(n)的大命令,导致Redis阻塞。 这里删除大key操作的思想也是如此。

先给键改名。

5.1 Delete Large Hash Key

通过hscan命令,每次获取500个字段,再用hdel命令,每次删除1个字段。Python代码:

def del_large_hash():
  r = redis.StrictRedis(host='redis-host1', port=6379)
    large_hash_key ="xxx" 
    cursor = '0'
    while cursor != 0:
        cursor, data = r.hscan(large_hash_key, cursor=cursor, count=500)
        for item in data.items():
                r.hdel(large_hash_key, item[0])

5.2 Delete Large Set Key

删除大set键,使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个键Python代码:

def del_large_set():
  r = redis.StrictRedis(host='redis-host1', port=6379)
  large_set_key = 'xxx'   
  cursor = '0'
  while cursor != 0:
    cursor, data = r.sscan(large_set_key, cursor=cursor, count=500)
    for item in data:
      r.srem(large_size_key, item)

5.3 Delete Large List Key

删除大的List键,未使用scan命令; 通过ltrim命令每次删除少量元素。Python代码:

def del_large_list():
  r = redis.StrictRedis(host='redis-host1', port=6379)
  large_list_key = 'xxx'  
  while r.llen(large_list_key)>0:
      r.ltrim(large_list_key, 0, -101) 

5.4 Delete Large Sorted set key

删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。Python代码:

def del_large_sortedset():
  r = redis.StrictRedis(host='large_sortedset_key', port=6379)
  large_sortedset_key='xxx'
  while r.zcard(large_sortedset_key)>0:
    r.zremrangebyrank(large_sortedset_key,0,99)

5.5 后台删除之lazyfree机制

为了解决redis使用del命令删除大体积的key,或者使用flushdb、flushall删除数据库时,造成redis阻塞的情况,在redis 4.0引入了lazyfree机制,可将删除操作放在后台,让后台子线程(bio)执行,避免主线程阻塞。

lazy free的使用分为2类:第一类是与DEL命令对应的主动删除,第二类是过期key删除、maxmemory key驱逐淘汰删除。

主动删除

UNLINK命令是与DEL一样删除key功能的lazy free实现。唯一不同时,UNLINK在删除集合类键时,如果集合键的元素个数大于64个(详细后文),会把真正的内存释放操作,给单独的bio来操作。

127.0.0.1:7000> UNLINK mylist
(integer) 1
FLUSHALL/FLUSHDB ASYNC
127.0.0.1:7000> flushall async //异步清理实例数据

注意:DEL命令,还是阻塞的删除操作。

FLUSHALL/FLUSHDB ASYNC

通过对FLUSHALL/FLUSHDB添加ASYNC异步清理选项,redis在清理整个实例或DB时,操作都是异步的。

127.0.0.1:7000> DBSIZE
(integer) 1812295
127.0.0.1:7000> flushall //同步清理实例数据,180万个key耗时1020毫秒
OK
(1.02s)
127.0.0.1:7000> DBSIZE
(integer) 1812637
127.0.0.1:7000> flushall async //异步清理实例数据,180万个key耗时约9毫秒
OK
127.0.0.1:7000> SLOWLOG get
 1) 1) (integer) 2996109
 2) (integer) 1505465989
 3) (integer) 9274 //指令运行耗时9.2毫秒
 4) 1) "flushall" 
 2) "async"
 5) "127.0.0.1:20110"
 6) ""

被动删除

lazy free应用于被动删除中,目前有4种场景,每种场景对应一个配置参数; 默认都是关闭。

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
lazyfree-lazy-eviction

针对redis内存使用达到maxmeory,并设置有淘汰策略时;在被动淘汰键时,是否采用lazy free机制;

因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。此场景使用时,请结合业务测试。

lazyfree-lazy-expire

针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazy free机制;

此场景建议开启,因TTL本身是自适应调整的速度。

lazyfree-lazy-server-del

针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。 此参数设置就是解决这类问题,建议可开启。

slave-lazy-flush

针对slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据场景,

参数设置决定是否采用异常flush机制。如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。

expire及evict优化

redis在空闲时会进入activeExpireCycle循环删除过期key,每次循环都会率先计算一个执行时间,在循环中并不会遍历整个数据库,而是随机挑选一部分key查看是否到期,所以有时时间不会被耗尽(采取异步删除时更会加快清理过期key),剩余的时间就可以交给freeMemoryIfNeeded来执行。

6、键值设计

key名设计

可读性和可管理性(建议)

以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id

set trade:order:1//业务名:表名:id

简洁性(建议)

保证语义的情况下,减低key长度,key过长也占用内存空间

user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid}

不要包含特殊字符(强制)

反例:包含空格、换行、单双引号以及其他转义字符

value设计

拒绝bigkey

在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey。

1.字符串类型:

它的big体现在单个value值很大,一般认为超过10KB就是bigkey。

2.非字符串类型:(hash,list,set,zset等)

哈希、列表、集合、有序集合,它们的big体现在元素个数太多。
一般来说hash、list、set、zset元素个数不要超过5000。
反例:一个包含200万个元素的list。

3.bigkey的删除

非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性)

7、优化bigkey

优化bigkey

1.一个字拆,大拆小

hash结构 比如一个big hash中有100万的数据可以通过key的名称做定义将100万的数据进行拆分成200个key,每个key中存放5000个数据
list结构也是同样操作,一个list的key存放5000个集合,拆开来存

2.避开危险操作

如果必须使用bigkey的话,那操作的时候避开hgetall、lrange、smembers、zrange、sinter等全数据查询的命令,有遍历的需求可以使用hscan、sscan、zscan代替(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。

3.合理使用数据类型(推荐)

例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡)
正例:

hmset user:1 name tom age 19 favor football

反例:

set user:1:name tom
set user:1:age 19
set user:1:favor football

4.控制key的生命周期,redis不是垃圾桶(推荐)

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。

命令使用

1.O(N)命令关注N的数量

例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。

2.禁用命令

禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。

3.合理使用select

redis自带的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。

4.使用批量操作提高效率

原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。

但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
注意两者不同:

原生命令是原子操作,pipeline是非原子操作。
pipeline可以打包不同的命令,原生命令做不到
pipeline需要客户端和服务端同时支持。

5.Redis事务功能较弱,不建议过多使用,可以用lua替代

posted @ 2021-01-13 11:26  MrSatan  阅读(7854)  评论(0编辑  收藏  举报