Redis过期策略

文章目录
  1. 1. Redis 过期策略
  2. 2. 内存淘汰机制
  3. 3. LRU vs LFU
  4. 4. LRU算法
  5. 5. LRU代码
  6. 6. 参考

Redis 过期策略

Redis 过期策略是:定期删除 + 惰性删除。

所谓定期删除,指的是 Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

假设 Redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 Redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 Redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。

但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,Redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。

但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 Redis 内存快耗尽了,怎么办?

答案是:走内存淘汰机制。

内存淘汰机制

Redis 内存淘汰机制有以下几个:

  • noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
  • volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
  • volatile-lfu 尝试淘汰设置了过期时间的 key,按最近的访问频率进行淘汰。
  • volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
  • volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
  • allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰(常用)
  • allkeys-lfu 区别于 volatile-lfu,这个策略要淘汰的 key 对象是全体的 key 集合,按最近的访问频率进行淘汰(常用)
  • allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。

如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。

如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。

LRU vs LFU

LFU 的全称是Least Frequently Used,表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。

Frequently [/‘friːkw(ə)ntlɪ/] adv.频繁地,屡次地

如果一个 key 长时间不被访问,只是刚刚偶然被用户访问了一下,那么在使用 LRU 算法下它是不容易被淘汰的,因为 LRU 算法认为当前这个 key 是很热的。而 LFU 是需要追踪最近一段时间的访问频率,如果某个 key 只是偶然被访问一次是不足以变得很热的,它需要在近期一段时间内被访问很多次才有机会被认为很热。

LRU算法

LinkedHashMap 默认的元素顺序是 put 的顺序,但是如果使用带参数的构造函数,那么 LinkedHashMap 会根据访问顺序来调整内部顺序。 LinkedHashMap 的 get() 方法除了返回元素之外还可以把被访问的元素放到链表的底端,这样一来每次顶端的元素就是 remove 的元素。

LRU代码

import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* 最近最少使用算法,linkedHashMap实现,主要是针对缓存过期策略实现
*
* @author renyapeng
*/
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> implements Serializable {

/**
* 缓存数据容量
*/
private int capacity = 16;

/**
* 默认装载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75F;

/**
* 带参数构造方法
*
* @param initialCapacity 容量
*/
public LRULinkedHashMap(int initialCapacity) {
/**
* @param initialCapacity 容量
* @param DEFAULT_LOAD_FACTOR 默认装载因子
* @param accessOrder 是否使用lru算法
* true:使用(基于访问顺序,get一个元素后,这个元素被加到最后,使用了LRU最近最少被使用的调度算法)
* false:不使用(基于插入顺序)
*/
super(initialCapacity, DEFAULT_LOAD_FACTOR, true);
// 传入指定的缓存最大容量
this.capacity = initialCapacity;
}

/**
* 实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
*
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

参考

《Redis 深度历险:核心原理与应用实践》 作者:钱文品

互联网 Java 工程师进阶知识完全扫盲