Redis过期策略

343 阅读8分钟

三种过期策略

策略含义优点缺点难点
定时删除在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除保证内存被尽快释放若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
惰性删除key过期的时候不删除,每次通过key获取值的时候去检查是否过期,若过期,则删除,返回null(用的时候再检查删除)删除操作只发生在通过key取值的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
定期删除每隔一段时间执行一次删除过期key操作通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点定期删除过期key--处理"懒汉式删除"的缺点在内存友好方面,不如"定时删除" 在内存友好方面,不如"定时删除"合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了),每次执行时间太长,或者执行频率太高对cpu都是一种压力。每次进行定期删除操作执行之后,需要记录遍历循环到了哪个标志位,以便下一次定期时间来时,从上次位置开始进行循环遍历

Redis采用的过期策略

因为Redis是单线程,收割过期key的时间会占用线程的处理时间,如果收割的操作太过于繁忙,可能会导致线上读写只指令出现卡顿。 所以redis采用的策略是: 定期删除 + 惰性删除

过期的key集合

Redis会将内分每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除了定时遍历之外,他还会使用惰性策略来删除过期的key。如果说定期删除时集中处理,那么惰性删除就是零散处理。

定期删除策略

Redis默认每秒进行10次过期扫描

  1. 从过期字典中随机选出20个key。
  2. 删除这20个key中已经过期的key。
  3. 如果过期的key的比例超过1/4,那就重复步骤1

同时,为了保证过期扫描不会出现循环过度,导致线程卡死的现象,算法还增加了扫面时间的上限,默认不会超过25ms。

假设一个大型的Redis实例中所有的key在同一时间过期了,会出现怎样的结果?
显然,Redis会持续扫描过期字典(循环多次),直到过期字典中过期的key变得稀疏,才会停止(循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另外一个原因是内存管理器需要频繁回收内存页,这就会产生一定的CPU消耗。
当客户端请求到来时,服务器如果正好进入过期扫描状态,客户端的请求将会等待至少25ms后才进行处理,如果客户端将超时时间设置的比较短,比如 10ms,那么就会出现大量的链接因为超时而关闭,业务端就会出现很多异常。而且这时你还无法从 Redis 的 slowlog 中看到慢查询记录,因为慢查询指的是逻辑处理过程慢,不包含等待时间。

所以如果有大批量的key过期,需要给过期时间设置一个随机范围,而不能全部在同一时间过期:

redis.expire_at(key,random.randint(86400) + expire_ts)
从节点的过期策略

从节点不会进行过期扫描,从节点对过期的处理是被动的。主节点在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从节点,从节点通过执行这条 del 指令来删除过期的 key。 因为指令同步是异步进行的,所以主节点过期的 key 的 del 指令没有及时同步到从节点的话,会出现主从数据的不一致,主节点没有的数据在从节点里还存在。

惰性删除

Redis为什么要惰性删除?

删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 删除包含大量键的数据库时,那么删除操作就会导致单线程卡顿。
redis 4.0 引入了 unlink 指令,它可以将删除键或数据库的操作放在后台线程里执行, 从而尽可能地避免服务器阻塞。

> unlink key
OK
flush

flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树连根拔起,扔给后台线程慢慢焚烧。

> flushall async
OK
异步队列

主线程将对象的引用从「大树」中摘除后,会将这个 key 的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作,所以必须是一个线程安全的队列。

redis-dead.drawio.png

不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样。

AOF Sync 也很慢

Redis需要每秒1(改数量可以设置)次同步AOF日志到磁盘,确保消息尽量不丢失,需要调用sync函数,这个操作比较耗时,会导致主线程的效率下降,所以Redis也将这个操作移到异步线程来完成。执行AOF Sync操作的线程是一个独立的异步线程,和前面的惰性删除不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放AOF sync任务。

更多异步删除点

Redis 回收内存除了 del 指令和 flush 之外,还会存在于在 key 的过期、LRU 淘汰、rename 指令以及从库全量同步时接受完 rdb 文件后会立即进行的 flush 操作。

Redis4.0 为这些删除点也带来了异步删除机制,打开这些点需要额外的配置选项。

  • slave-lazy-flush 从库接受完 rdb 文件后的 flush 操作
  • lazyfree-lazy-eviction 内存达到 maxmemory 时进行淘汰
  • lazyfree-lazy-expire key 过期删除
  • lazyfree-lazy-server-del rename 指令删除 destKey

内存淘汰机制 —— LRU

当redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap)。交换会让Redis的性能急剧下降,对于访问量比较大的Redis来说,这样龟速的存取效率基本上等于不可用。
Redis提供了配置参数maxmemory来限制内存超出期望大小。
当实际内存超出maxmemory时,Redis提供了几种可选策略(maxmemory-policy)来让用户决定该如何腾出新的空间以继续提供读写服务。

  • noeviction: 当内存超出 maxmemory,写入请求会报错,但是删除和读请求可以继续。(默认策略)
  • volatile-lru: 当内存超出 maxmemory,在设置了过期时间 key 的字典中,移除最少使用的 key。把 Redis 既当缓存,又做持久化的时候使用这种策略。
  • volatile-random: 当内存超出 maxmemory,在设置了过期时间 key 的字典中,随机移除某个key。
  • volatile-ttl: 当内存超出 maxmemory,在设置了过期时间 key 的字典中,优先移除 ttl 小的。
  • allkeys-lru: 当内存超出 maxmemory,在所有的 key 中,移除最少使用的key。只把 Redis 既当缓存是使用这种策略。
  • allkeys-random: 当内存超出 maxmemory,在所有的 key 中,随机移除某个 key。

如果你只是拿Redis做缓存,那么应该使用allkeys-xxx策略,客户端写缓存时不必携带过期时间; 如果还想同时使用Redis的持久化功能,那就是用volatile-xxx策略,这样可以保留没有设置过期时间的key,他们是永久的key,不会被LRU算法淘汰;