Redis Big Key

281 阅读11分钟

1、bigkey带来的问题

  1. 内存消耗过大: 大键可能占用大量的内存,导致内存不足或碎片化,影响 Redis 服务器的性能和稳定性。
  2. 性能问题: 大键的操作可能会影响 Redis 的性能。执行某些操作(如获取大键的全部内容或范围内的内容)可能会消耗较多的时间和资源,导致 Redis 的响应变慢。
  3. 阻塞其他操作: 在执行某些大键操作时,特别是在执行 HGETALLLRANGEGETHMGET 等命令时,会导致 Redis 实例被阻塞,进而影响其他命令的执行,可能导致 QPS 突然上升或下降,引起不稳定性。
  4. 网卡和 CPU 瓶颈: 大键操作可能导致网卡成为瓶颈,尤其是在集群环境下。此外,在 Redis 的单线程模型中,大键操作容易导致 CPU 瓶颈,因为大键的操作可能会耗费更多的 CPU 资源。
  5. 删除操作阻塞: 删除大键时可能会导致实例被阻塞,特别是使用 DEL 命令。这会导致被阻塞的实例无法响应其他请求,而阻塞的时间随着大键的大小而增加。

2、bigkey是如何产生的

  1. 大量数据写入单个键: 当向 Redis 中的一个键中写入大量数据时,例如使用 SET 命令或者其他数据结构(如 Hash、List)的操作,会导致该键变得非常大。
  2. 大型数据结构的操作: 执行某些 Redis 命令,如 LPUSHRPUSHHSETSADD 等操作时,如果对同一个键进行大量的数据添加,可能导致该键的体积急剧增大。
  3. 缓存数据滞留: 如果 Redis 作为缓存使用,某些热点数据或者过期数据没有被及时清理或更新,就可能会导致某个键的尺寸异常增大。
  4. 数据存储模型设计不合理: 在设计数据模型时,如果没有合理地拆分数据,将大量数据存储在一个键中,可能会导致单个键变得庞大。
  5. 大型复杂操作: 在一些业务场景下,执行一些复杂操作(例如数据导入、批量操作等)可能会导致某个键在短时间内变得巨大。

3、查找bigKey的方法

  1. SCAN 命令: 使用 SCAN 命令迭代键空间,逐步获取所有键,然后评估键的大小。虽然该方法可能会影响性能,但可以帮助找出潜在的大键。
  2. OBJECT 命令: OBJECT 命令可以查看特定键的类型和空间占用情况,如 OBJECT ENCODING key 可以查看键值的编码类型,OBJECT IDLETIME key 可以查看键的空闲时间,OBJECT REFCOUNT key 可以查看键被引用的次数。
  3. MEMORY USAGE 命令: 使用 MEMORY USAGE key 命令可以获取指定键的内存占用情况,从而找出占用较大内存的键。
  4. Redis4.0 及以上版本提供了--Bigkeys 命令,可以分析出实例中每种数据结构的top 1 的Bigkey

4、直接删除bigkey的风险

  1. 阻塞问题: 删除大键时,Redis 会阻塞其他客户端的请求直到删除完成。这可能会影响其他操作的执行速度,并在某些情况下导致系统变慢。
  2. 性能问题: 如果大键很大,删除操作可能需要一些时间来完成。这可能会影响 Redis 实例的性能和响应时间。
  3. 数据丢失风险: 删除大键将永久删除其中存储的数据,因此请务必谨慎操作,确保不会丢失需要的数据。 DEL命令在删除单个集合类型的Key时,命令的时间复杂度是O(M),其中M是集合类型Key包含的元素个数。

生产环境中遇到过多次因业务删除大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

  1. 分析大键: 首先,了解大键的结构和大小,可以使用 Redis 的命令来查看键的类型以及包含的元素数量。例如,使用 TYPE key 查看键的类型,使用 STRLEN key 查看字符串的长度,或者使用 LLEN key 查看列表的长度。

  2. 按部就班逐步删除: 对于大型列表或哈希表,可以使用分批次的方式逐步删除其中的元素。例如,对于列表,可以使用 LPOPRPOP 命令逐个删除列表元素。

    bashCopy code
    # 逐个删除列表中的元素
    while (redis-cli LLEN key)
    do
      redis-cli LPOP key
    done
    
  3. 分割键或迁移数据: 对于大型哈希表,如果键中包含大量字段,可以考虑将其拆分为多个小键或者将部分数据迁移到其他键中。这样可以更容易地管理和操作数据。

  4. 使用过期时间: 如果适用,可以为大键设置适当的过期时间。例如,使用 EXPIRE key seconds 设置键的过期时间,Redis 将在过期后自动删除该键。

  5. 使用专门工具: 有一些 Redis 监控和管理工具可以帮助发现和处理大键。例如,Redis 的 RedisGears 模块提供了大键扫描器,可以扫描和处理大键。

  6. 使用后台任务: 在不影响正常操作的情况下,可以考虑使用后台任务来处理大键的删除操作,以避免影响 Redis 实例的性能。

5.1 Delete Large Hash Key

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

def del_large_hash():
  r = redis.StrictRedis(host='redis', 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

简洁性(建议)

保证语义的情况下,减低key长度

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

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

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的可能性)