前言
在 Redis 中可以通过命令 expire 对指定的 key 值设置过期时间,在时间到了以后该键值对就会自动删除。
一个 Redis 中可能会存在很多的 key ,而这些 key 中有很大的一部分都会有过期时间,那么 Redis 怎么知道哪些 key 已经到了过期时间需要删除,哪些 key 还没到过期时间呢?
有读者可能会说,遍历不就得了,遍历 Redis 中所有的数据,就知道哪些数据已经到时间需要删除,哪些数据还没到时间。但这个方法显然是不行的,因为遍历一遍 Redis 中所有的数据非常的消耗时间和资源,而且还需要不停的遍历。
实现策略
redis 整体的策略是:1.惰性删除 2.定期删除
惰性删除
当一个 key 已经到过期时间时,暂时不删除该键值对,直到下次用户访问这个 key 值,才发现该 key 值已经过期,系统就顺便将该键值对删除,并且返回给用户 nil ,表示数据不存在。
定期删除
规定一个定期删除数据的时间,比如每周删一次,并且删除过程中不是遍历 Rrdis 中所有的键值对,而是抽取一部分键值对,将其中过期的键值对进行删除,要保证定期删除的过程要足够快。
为什么对定期删除的速度有较高的要求呢?
因为如果定期删除需要遍历 Redis 中所有的数据,将全部过期的键值对都找出来进行删除,那么在 Redis 中的数据很多的情况下,这个过程就会持续很久,而 Redis 是单线程的程序,此时在进行定期删除时,无法同时处理用户传来的请求,这就会导致阻塞,Redis 通常作为缓存,那么如果 Redis 出现了阻塞,就会导致大量的用户请求涌入数据库,很可能导致数据库直接挂掉,会造成很大的损失。
补充
虽然有了上述的两种策略结合但整体的效果一般,仍然可能有许多过期的 key 值残留,没有被及时删除掉,redis 为了对上述进行补充,还用到了许多的内存淘汰策略
定时删除
可能有某些文章会提到 Redis 使用了定时删除的策略,但这里明确的说,Redis 中并没有用到定时删除的策略,但定时删除也是可以实现 key 的过期策略的,而且也比较优秀,这里来简单介绍一下
定时器:在指定的时间到达后,执行对应的任务
假设在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时将 key 删除
优点:保证内存被尽快的释放
缺点:如果过期的 key 很多,删除这些 key 就会消耗很多的 CPU 资源。定时器的创建很消耗时间和资源,若为每一个有过期时间的 key 都创建一个定时器,将会有大量的定时器产生,性能影响严重
要想高效的通过定时删除实现 key 的过期策略,有两个比较优秀的方法
1.基于优先级队列/堆
优先级队列也叫堆,分为大根堆和小根堆,堆顶的数据是最大/最小的。
假设现在有许多的 key 都设置了过期时间,将这些 key 都添加到优先级队列中,指定优先级规则为 key 的过期时间最早的在堆顶,也就是以 key 的过期时间来建立小根堆
由于在优先级队列中,堆顶的 key 是过期时间最早的,所以只要堆顶的 key 还没过期,其他的 key 也不会过期,此时定时器中只要分配一个线程,让这个线程去检查堆顶元素是否过期即可,此时线程不需要遍历所有的 key
另外,线程检查堆顶元素是否过期也不能检查得太频繁,因为要是不停的去检查堆顶元素是否过期浪费了许多的 CPU 资源,好的做法是,根据当前时刻和堆顶元素的过期时间来设置一个等待时间,当等待时间到了以后,再唤醒线程
但此时会出现一个问题,如果在线程等待的时候,有一个新的 key 值插入堆,并且过期时间比堆顶 key 的过期时间还早,那么线程要是还继续等待的话,堆顶的 key 已经过期很久了,所以当有新的 key 插入时,要唤醒线程,key 插入以后重新根据当前的时间和堆顶 key 的过期时间来设置线程的等待时间
2.基于时间轮来实现定时删除
把时间划分成很多的小段,划分的粒度看具体的要求,每个小段上都挂着一个链表,每个链表表示一个要执行的任务,假设需要添加一个 key,这个 key 在 300 ms 后过期,那么这个 key 就会挂在 3 号链表上,如果 key 的过期时间很长,假设有 600 ms,那么就循环,挂在 1 号 链表上
在时间轮上,有一个指针,从 1 号链表开始遍历,每走到一个链表,就把这个链表上的 key 检查一下,看有没有过期的,有过期的就删除,然后继续遍历。