前言
redis是一个内存数据库。
我们set key的时候,都可以给一个expire time,就是过期时间。
如果假设你设置一批key只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
redis内存满了之后会发生什么事情呢?
生产上应该设置哪种缓存淘汰策略呢?
好了,今天我们就来讲讲redis的缓存淘汰策略。
redis三种删除策略
1.定时删除
定时删除也就是立即删除。
在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
立即删除能够保证内存被尽快释放。但是这样的话对cpu很不友好,因为删除操作会占用cpu时间,所以如果过期的key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,也会影响数据的读写操作。
总结:用cpu性能换取内存空间(时间换空间)。
2.惰性删除
当数据到达过期时间时,先不做处理。等到下次访问该数据时,如果数据已过期,再对数据进行删除。
这种删除策略问题也很明显,如果内存中有大量过期key。但是一直没人访问,那么数据就不会过期,内存也不会释放。发生内存泄露。
总结:用内存换取cpu处理时间(空间换时间)。
3.定期删除
定时删除是对CPU和内存消耗取得一个折中方案。
每隔一段时间执行一次删除过期key操作。
通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用(处理"定时删除"的缺点)
定期删除过期key(处理"惰性删除"的缺点)
难点
合理设置删除操作的执行时长和执行频率(要根据具体服务器运行情况来定)
redis使用的哪种过期策略?
定期删除+惰性删除
redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
上面说过定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就用到了惰性删除了,也就是说,在你获取某个key的时候,redis会检查一下 ,这个key如果过期了此时就会删除,不会给你返回任何东西。
通过上述两种手段结合起来,保证过期的key一定会被干掉。
但是这样还是有问题的,如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时大量过期key堆积在内存里,导致redis内存快耗尽了,咋整?
下面就轮到redis的内存淘汰机制了。
redis内存淘汰策略
当redis的内存占用过多的时候,此时会进行内存淘汰,redis6以后有如下一些策略
规则名称 | 规则说明 |
noeviction | 当内存不足以容纳新写入数据时,新写入操作会报错 |
allkeys-lru | 对所有key使用lru算法进行删除 |
allkeys-lfu | 对所有key使用lfu算法进行删除 |
allkeys-random | 对所有key随机删除key |
volatile-lru | 对设置了过期时间的key使用lru算法进行删除 |
volatile-lfu | 对设置了过期时间的key使用lfu算法进行删除 |
volatile-random | 对设置了过期时间的key使用随机删除 |
volatile-ttl | 删除快要过期的key |
LRU算法
LRU算法又叫淘汰算法,根据数据历史访问记录进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
即删除最近最少使用的缓存。
我们用java实现一个demo看看
package com.fandf.test.redis;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* lru算法demo
* 最近最少使用算法
*
* @author fandongfeng
* @date 2023/4/2 17:30
*/
@Slf4j
public class LRUCache<K, V> {
private final Map<K, V> cache = new HashMap<>(8);
private final LinkedList<K> lruList = new LinkedList<>();
private final Integer capacity;
public LRUCache(Integer capacity) {
this.capacity = capacity;
}
public static void main(String[] args) {
test();
}
private static void test() {
LRUCache<String, Object> cache = new LRUCache<>(2);
cache.put("第一天", "好好学技术");
cache.put("第二天", "我想出去玩");
log.info("第一天数据 => {}", cache.get("第一天"));
log.info("第二天数据 => {}", cache.get("第二天"));
cache.put("第三天", "学不动了");
//这里调用了 cache.get("第二天"),所以第二天变为最新使用过得缓存
log.info("放入第三天数据,第二天数据 => {}", cache.get("第二天"));
//这里调用了cache.put("第四天", "马什么梅?");,所以最后只剩了第二天和第四天
cache.put("第四天", "马什么梅?");
log.info("放入第四天数据");
log.info("第一天数据 => {}", cache.get("第一天"));
log.info("第二天数据 => {}", cache.get("第二天"));
log.info("第三天数据 => {}", cache.get("第三天"));
log.info("第四天数据 => {}", cache.get("第四天"));
}
/**
* 获取元素
*
* @param k key
* @return value
*/
public V get(K k) {
if (!cache.containsKey(k)) {
return null;
}
V v = cache.get(k);
// 更新 k 在LRU 队列中的位置
if (!lruList.getFirst().equals(k)) {
//移除K,在放入队头
lruList.remove(k);
lruList.addFirst(k);
}
return v;
}
/**
* 存入元素
*
* @param k Key
* @param v value
*/
public void put(K k, V v) {
// 判断容量
if (lruList.size() >= capacity && !lruList.contains(k)) {
// 移除最近最少使用的数据
cache.remove(lruList.getLast());
lruList.removeLast();
}
cache.put(k, v);
//放入队头
if (!lruList.contains(k)) {
lruList.addFirst(k);
}
}
}
缺点:假设我们有两个key, key1和key2,我访问了一万次key1,然后访问了一次key2,此时如果使用lru算法,那么就会将key2淘汰。
LFU算法
LFU(Least Frequently Used)是Redis 4.0 引入的淘汰算法,它通过key的访问频率、访问时间比较来淘汰key,重点突出的是Frequently Used。
LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
应该使用哪种?
redis默认的内存淘汰策略是noeviction,即内存不足时,写入直接报错。这种方式往往会导致服务不可用,但是往往即使是生产环境,很多人在使用的时候都不会修改内存淘汰策略。
生产上建议根据情况选择不同的策略使用。如果不知道使用哪种?个人建议还是使用allkeys-lru吧。怎么说呢,中规中矩吧。