本文转载自博客:https://blog.csdn.net/SnailMann/article/details/97385084
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
一起学习Redis | 聊聊Redis的LRU内存淘汰算法?
如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里
一起学习Redis | 聊聊Redis的数据过期删除策略?
一起学习Redis | 聊聊Redis的LRU内存淘汰算法?
学习Redis的内存淘汰算法之前,我们可以先了解Redis的数据过期删除策略
前提概要
常见问题
为什么需要内存淘汰算法?
Redis的内存淘汰算法
Redis提供的六种内存淘汰策略
说说LRU算法
什么是异步删除?
手撸LRU算法
Java版本
Python版本
相关问题
Redis的LFU内存淘汰算法
我刚插入到Redis中不久的数据怎么没啦?
我明明给Redis的数据设置了过期时间,时间到了,为什么那些数据还占用着内存?
前提概要
常见问题
Redis的过期策略有哪些?
聊一聊Redis的内存淘汰算法?
手写一个LRU代码实现?
我插入到Redis中的数据怎么没啦?
我明明给Redis的数据设置了过期时间,时间到了,为什么那些数据还占用着内存?
为什么需要内存淘汰算法?
讲一个小场景
一名开发人员经常吐槽Redis有Bug,说他们的生产环境中的Redis经常会丢掉一些数据。大概就是插进Redis没多久,再查就不见了,很不靠谱。这样的问题不是Redis不靠谱,而是这名开发人员没有意识到内存存储是有上限的,他们生产环境的Redis所存储的数据过多,已经超过了配置的内存大小,所以正在执行内存淘汰算法呢。
为什么要内存淘汰算法
我们知道,内存读写速度快,非常好用,但是就是贵,所以内存空间是宝贵的。而我们的Redis是基于内存的的存储系统,所以也会有一个内存存储空间的上限。并不是说你想用多少就多少的,比如你的Redis中的数据最多存储16G,此时你要是往里写入了20G的数据,会发生什么?那自然是有选择的从这20G数据从挑选出16G进行存储,多的就没办法了,这是物理限制。那么怎么去挑选呢?该淘汰那些数据呢?这就涉及到内存淘汰算法啦
Redis的内存淘汰算法
Redis提供的六种内存淘汰策略
当我们的数据实际内存大小超出了Redis所允许的最大内存大小时,Redis提供了几种可选的内存淘汰策略(maxmemory-policy)。
noeviction
对外停止提供写服务,只允许读,删除等操作进行。保证已有数据不丢失,但影响了写服务的可用性。这是默认的淘汰策略
volatile-lru
从有过期时间的key中淘汰最少使用的key。既认为有过期时间的数据,不如没有过期时间的数据重要
volatile-ttl
从有过期时间的key中淘汰ttl时间还剩最少的key, 既越快过期的优先淘汰
volatile-random
从有过期时间的key中随机淘汰数据
allkeys-lru
从所有key中,执行lru策略,淘汰最少使用的key
allkeys-random
从所有key中,执行random策略,随机淘汰数据
说白了就是分为三大类,停止服务,从有过期时间的数据中淘汰,从所有数据中淘汰, 具体又分为三种策略
lru 淘汰最少使用的数据
ttl 淘汰寿命还剩最少的数据
random 随机淘汰数据
说说LRU算法
(一) 普通的LRU淘汰算法
我们知道LRU算法是淘汰最少使用的数据,怎么我们怎么知道哪些数据是最少使用的呢?
LRU算法的实现,需要一个字典和一个有序链表进行组合,有点类似Redis的zset
字典是用来存储数据的,链表是根据最近访问来对数据进行排序的,一端是最近访问的,另一端是最久访问的,比如链尾是最近访问,链头是最久访问
链表中的元素按照一定的顺序进行排序。当空间满的时候,会踢掉链头的元素。当字典的某个元素被访问,则它会从链表的原来位置移动到链表尾部。
所以我们知道了,位于链表头部的元素就是没这么重要的元素,毕竟都这么久没被访问了,优先淘汰它。位于链尾的元素属于最近才刚被访问的元素,算是热点数据,所以暂时不会淘汰它
(二) 近似LRU淘汰算法
虽然LRU淘汰算法很棒,但是Redis所使用的LRU算法并非我们通常所说的LRU算法,而是一种近似LRU算法。这种近视LRU算法的实现原理跟LRU算法不同,是以别的实现原理实现的一个结果接近LRU算法的算法,所以叫近似LRU算法。
为什么Redis不直接使用通常意义的LRU算法呢? 因为通常LRU算法需要消耗大量的额外内存,需要对现有的数据结构进行较大的改造。而近似LRU算法相对简单,允许基于Redis现有的数据结构,使用随机采样法来淘汰元素。
Redis为了实现近似LRU算法,给每个key增加了一个额外的24bit小字段,用来记录该key最后一次被访问的时间戳、
近似LRU算法会根据具体的Redis淘汰策略,从有过期时间的数据或全部数据中,随机采用n个数据,采用淘汰掉最旧的数据。依次循环,直到Redis内存使用情况处于正常范围。
所以我们知道了,Redis实际的LRU算法是一种在结果上模拟出LRU算法样子的近似LRU算法。其淘汰数据所删除的删除策略是惰性删除。不同于过期时间的删除策略(集中删除 + 惰性删除)
什么是异步删除?
我们都知道Redis是单线程模型,但实际上Redis内部并非真的只有一个Redis线程,而是一个主线程执行通常的业务操作,但它还会有好几个异步线程专门做一些耗时的操作
删除指令del会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果被删除的key是一个大对象,比如一个包含了百万元素的hash或set。那么这样的删除就会导致单线程卡顿了。所以Redis为了解决类似的问题,在4.0版本引入了unlink指令,它能对删除操作进行异步处理,丢给后台线程来异步回收内存。
类似的单线程卡顿操作还有很多,比如flushdb,flushall等清空数据库的操作,都可以使用flush ansy等方式,将要删除的数据丢到一个消息队列中,让后台异步线程从消息队列中慢慢拿要删除的数据,异步删除处理。不阻塞主线程的其他业务操作。
手撸LRU算法
Java版本
我们这里不写原生的LRU实现,所以通过继承LinkedHashMap来简单的实现LRU内存淘汰算法。
package com.snailmann.learn.lru;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 通过linkedHashMap来模拟LRU内存淘汰算法
*
* @param <K>
* @param <V>
*/
public class LRUCache2<K, V> extends LinkedHashMap<K, V> {
/**
* LRU容器最大的元素个数
*/
private final int cacheSize;
/**
* LinkedHashMap本身就支持根据访问顺序排列数据
* 最先访问在队头,最久访问在队尾
* accessOrder = true就是顺序访问排序,false就是插入顺序排序,默认是false
*
* @param cacheSize
*/
public LRUCache2(int cacheSize) {
/**
* 为了让LRUCache的size是我们设置的size,就需要知道好内部的hashmap的size, 既size / hashmap加载因子就是hashmap的size
* 再 + 1 是为了不让hashmap扩容
*/
super((int) (Math.ceil(cacheSize / 0.75) + 1), 0.75f, true);
this.cacheSize = cacheSize;
}
/**
* 这是LinkedHashMap本身的判断,是否删除队尾元素,true删除,false不删除
* 我们要重写它,只有size超了,才删除队尾元素
*
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
public static void main(String[] args) throws InterruptedException {
LRUCache2<Integer, Long> map = new LRUCache2<>(10);
for (int i = 0; i < 10; i++) {
long time = System.currentTimeMillis();
map.put(i, time);
Thread.sleep(100);
}
System.out.println(map);
for (int i = 10; i < 16; i++) {
long time = System.currentTimeMillis();
Integer key = i;
map.put(key, time);
}
System.out.println(map);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Python版本
from collections import OrderedDict
class LRUDict(OrderedDict):
"""
通过OrderedDict实现LRU
OrderedDict本质就是dict + 双向链表
"""
def __init__(self, capacity, **kwargs):
"""
:param capacity: LRUDict的最大容量
:param kwargs:
"""
super().__init__(**kwargs)
self.capacity = capacity
self.items = OrderedDict()
def __setitem__(self, key, value):
old_value = self.items.get(key)
# 如果key键已存在,则先弹出该键值对,重新插入新值,同时存储位置也换了
if old_value is not None:
self.items.pop(key)
self.items[key] = value
# 如果LRUDict的容量够,且key不存在容器中,则直接插入
elif len(self.items) < self.capacity:
self.items[key] = value
# 如果LRUDict容量不够,且key不在容器中,则执行LRU淘汰算法,弹出队头元素,新元素插入队尾
else:
self.items.popitem(last=False)
self.items[key] = value
def __getitem__(self, key):
value = self.items.get(key)
if value is not None:
self.items.pop(key)
self.items[key] = value
return value
def __repr__(self):
return repr(self.items)
lru = LRUDict(10)
for i in range(1, 15):
lru[i] = i
print(lru)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
相关问题
Redis的LFU内存淘汰算法
因为本章节重点讲的是LRU淘汰算法,所以就没有提及LFU算法。但是实际上LFU算法可以说是比LRU算法更优秀的淘汰算法。
什么是LFU算法?
LFU淘汰算法,既Least Frequently Used, 表示按最近访问频率对数据进行内存淘汰,它比LRU更加准确的表示了一个key被访问的热度
如果一个Key长时间不被访问,只是刚刚偶然被用户访问了一下,那么LRU算法也会认为它是一个热点数据,所以不会淘汰它。而LFU算法则需要追踪最近一段时间的访问频率,如果某个key只是偶然被访问一次是不足以让LFU认为这是一个热点数据的。它需要在近一段时间被访问很多次,才有机会成为LFU认可的热点数据
怎么开启LFU模式?
Redis 4.0在淘汰策略配置参数maxmemory-policy中又添加了两个选项:
volaitle-lfu
allkeys-lfu
这两种再加上以前的6种,相当于已经有了8中淘汰策略选项了!! Redis还是提供了很多种策略给我们选择的。
我刚插入到Redis中不久的数据怎么没啦?
Redis是内存存储系统,通常我们是把Redis当做缓存使用的,什么是缓存?意思就是这些数据很可能会随时不见的,它并不能简单的当做是一个持久化的数据库存储系统!
如果你出现了数据消失的情况,一就是检查,是否设置了过期时间。而就是检查Redis内存使用情况,是否因为满了导致执行内存淘汰算法。
我明明给Redis的数据设置了过期时间,时间到了,为什么那些数据还占用着内存?
因为Redis采用的是定时删除和惰性删除两种删除策略。既如果我们的Redis数据过期时间到了,Redis是并不会立马从内存中删除这些数据的。而是采用两种策略是删除:
定时删除
默认每100ms对有过期时间的数据进行一次扫描,每次扫描只扫描一部分数据。只要发现过期数据,此时立即删除。但是由于定时删除每次删除的是部分数据,所以并不代表定时的每次删除都能把所有过期数据删除完,这需要一定的时间去清理
惰性删除
虽然定时删除每隔100ms一次,1s就至少执行了10次。但当设置了过期时间的数据量比较庞大的时候,定时删除也总会漏掉一下过期的数据,导致这些过期数据没有删除,依然遗留在内存中。此时就需要依靠惰性删除机制。既当这些过期数据被再次访问时,Redis会检查这些数据是否过期,如果已经过期,就立即删除
总之我们可以知道,Redis的过期数据并非都能得到立即的删除,所以我们就会发现即使数据的过期时间到了,但这些数据依然会占用内存空间一段时间。