Redis-大key删除法

转载:redis随手记-大key删除法 - 墨天轮 (modb.pro)

问题

redis大key是让人比较头疼的问题,如果线上redis出现大key,断然不可立即执行del
,因为大key的删除会造成阻塞。阻塞期间,所有请求都可能造成超时,当超时越来越多,新的请求不断进来,这样会造成redis连接池耗尽,尽而引发线上各种依赖redis的业务出现异常。

做个简单测试

通过脚本先向redis写入大量的数据:

127.0.0.1:6379> hlen hset_test
(integer) 3784945

这里看到大概有300多万的数据,我们执行个del
看看:

127.0.0.1:6379> del hset_test
(integer) 1
(3.90s)

可以发现耗时将近4s

我们知道redis核心是单线程在跑的,那么这个阻塞期间,redis是无法处理其他请求的。

低峰期删除

最简单的方式就是在业务低峰期进行删除,比如大部分场景在凌晨4点左右比较低峰,这时候执行删除,造成的影响比较小。当然这种方式也是无法避免阻塞期间的请求,一般适用执行期间qps非常小的业务。

scan分批

既然大key不能一下删除,那么我们就分批删除。

hset

对于hset,我们hsan分批删除。

# 伪代码
HSCAN key 0 COUNT 100
HDEL key fields

每次取个100条,然后删除

set

对于set,我们可以每次随机取一批数据,然后删除

# 伪代码
SRANDMEMBER key 10
SREM key fields

zset

对于zset,每次可以直接删除一批数据

伪代码
ZREMRANGEBYRANK key 0 10

list

对于list,直接pop

伪代码
i:=0
for {
    lpop key
    i++
    if i%100 == 0 {
        sleep(1ms)
    }
}

异步删除

过期key删除策略

有人说既然在线删除大key会造成阻塞,那么就对这个key设置一个TTL,交给redis自己去删。我先看看redis的过期key删除策略:

定期删除:

我们知道redis的key分为带过期的和永久的,对于有过期时间的key,redis会单独放在一个字典表里,单独的好处就是redis知道这个字典里的key随时可能过期,那么我就定期过来处理下,定期的任务就交给了serveCron,默认每100ms执行一次。每当serveCron执行的时候,就会去带ttl的key里面随机抽取一部分key来检查,如果这批key真的过期了,那么就执行同步删除。随机抽查的原因:

  1. 不可能全部检验的,阻塞线程
  2. 随机的话体现一定的公平性

惰性删除

通过定期删除,我们可以每次删除一批已经过期的key,但是如果一个key已经过期了,定期删除也没清理到,这时用户来读取这个key的话,肯定不能直接返回,这时也会检查这个key是否过期,如果过期直接删除,返回空。

淘汰策略

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  2. allkeys-lru:通过LRU算法驱逐最久没有使用的键
  3. volatile-lru:通过LRU算法从设置了过期时间的键集合中驱逐最久没有使用的键
  4. allkeys-random:从所有key中随机删除
  5. volatile-random:从过期键的集合中随机驱逐
  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  8. allkeys-lfu:从所有键中驱逐使用频率最少的键

淘汰策略是一个灵活的选项,一般根据业务来选择合适的淘汰策略,那么自定义的淘汰策略是何时触发的?当然是我们进行加key或者更新一个更大的key的时候。所以他的删除也是同步的,如果正好淘汰一个大key的时候,很不幸当前也会发生阻塞。

总结:不管以上三种哪个触发的删除,它都是同步的。所以就算加个TTL,redis也是同步删除的,大key还是会造成阻塞。

异步删除

在redis4.0的时候,作者对于大key删除造成阻塞的问题也做了考虑,于是出现了异步删除,异步删除也分为用户主动和程序被动。

主动删除

unlink

对于主动删除,redis提供了del
的替代方法unlink
,当我们在unlink的时候,redis会先检查要删除元素的个数(比如集合),如果集合的元素的小于等于64个的时候,就会直接执行同步删除,因为这不算一个大key,不会浪费很多的开销,但是当超过64个的时候,redis会认为是大key的概率比较大,这时候redis会在字典里,先把key删除,真正的value会交给异步线程来操作,这样的话就不会对主线程造成任何影响。

flushall、flushdb

在执行flushall或者flushdb的时候,增加了ASYNC选项 FLUSHALL [ASYNC]
,当用户没设置ASYNC的时候,此时的flush操作是阻塞的,当设置了ASYNC的时候,会建立一个新的空字典,然后指向它,老字典交给异步线程来慢慢删。

被动删除

redis配置策略

  • lazyfree-lazy-eviction:针对redis有设置内存达到maxmemory的淘汰策略时,这时候会启动异步删除,此场景异步删除的缺点就是如果删除不及时,内存不能得到及时释放。
  • lazyfree-lazy-expire:对于有ttl的key,在被redis清理的时候,不执行同步删除,加入异步线程来删除。
  • replica-lazy-flush:在slave节点加入进来的时候,会执行flush清空自己的数据,如果flush耗时较久,那么复制缓冲区堆积的数据就越多,后面slave同步数据较相对慢,开启replica-lazy-flush后,slave的flush可以交由异步现成来处理,从而提高同步的速度。
  • lazyfree-lazy-server-del:这个选项是针对一些指令,比如rename一个字段的时候 RENAME key newkey
    , 如果这时newkey是存在的,对于rename来说它就要删除这个newkey的value,如果这个newkey是一个大key,那么就会造成阻塞,当开启了这个选项时也会交给异步线程来操作,这样就不会阻塞主线程了。
题外话:rename

先来做个测试:

127.0.0.1:6379> set A 1
OK

127.0.0.1:6379> eval "for i=1,10000000,1 do redis.call('hset','B', i,1) end" 0
(15.89s)
  1. 设置A为1
  2. 向B里面添加1000w的数据

B肯定是大key了,这时想把A重新命名成B执行rename A B

127.0.0.1:6379> rename A B
OK
(11.07s)

发现阻塞了,这是因为redis删除B造成的,如果有rename的场景一定要注意newkey是否已经存在,newkey是否是大key。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐