🤔
过期删除和内存淘汰乍一看很像,都是做删除操作的,这么分有什么意思?
首先,设置过期时间我们很熟悉,过期时间到了,我么的键就会被删除掉,这就是我们常认识的过期删除,但是实际上的过期删除真的是这样吗?
1 过期删除策略
实际上,一般过期之后的键不是立刻删除的,一般过期键的清除策略有三种,分别是**定时删除
、定期删除
和惰性删除
**。
1.1 定时删除
定时删除 时在设置键的**过期时间
**的同时,创建一个定时器
,让定时器在键过期时间来临时,立即执行对键的删除操作
。
定时删除看起来和我们原来想象的一样,这样对内存
来说也确实比较友好
,但是对CPU不友好
,如果某个时间段比较多的key过期
的话,可能会影响命令处理性能。
1.2 惰性删除
所谓惰性 就是不要那么勤快,随时都盯着,用的时候发现不对再去删就行了,具体就是使用的时候发现key过期了,此时再进行删除。这个策略的思路是对应用而言,只要不访问,过不过期对于业务而言都无所谓,但是这样也是有代价的,就是,如果某些key一直不来访问,那么本该过期的key,就变成常驻的key了,这种策略对CPU友好
,对内存不友好
。
1.3 定期删除
定期删除就是每隔一段时间,程序就对数据库进行一次检查,每次删除一部分过期键。
定时删除实际实现起来非常不容易,主要如果出现了一场,可能会有key遗漏,以及如果程序重启,原来的定时器就随之重启消失了,那就需要在启动时对过期的键进行进行一些操作,可能是重建定时器,这些都是额外的工作,而且会引入多余的复杂度。
从实际的功能而言,其实并不需要那么实时,所以**惰性删除是
**可以考虑的,但是出于应删尽删的考虑,要保证最终没有漏网之鱼,那有没有这样的策略呢?
有的有的,兄弟有的,加上**定期删除
**作为兜底就可以了。所以Redis过期键采用的删除策略是惰性删除+定期删除二者结合的方式进行,这样就可以以一定CPU消耗换取对内存的友好
1.3.1 定期删除需要关注的两个问题:
-
定期删除的频率:
- 这取决于Redis周期任务的执行频率,周期任务里面会做关闭客户端,删除过期key的一系列任务,可以用INFO查看周期任务频率
-
每次删除的数量:
-
随机选取20个key判断是否过期,同时检查过期key数量占比,如果
>25%
,则再抽20个重复上述流程,这里是一个循环的过程。 -
Redis为了保证定期删除不会出现循环过度导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不过超过25ms。
-
前面我们说到,过期删除和内存淘汰的区别是什么,都是做删除操作的?
我们先看它们解决的问题分别是什么?过期删除策略解决的是:过期的key怎么删除?内存淘汰策略解决的是:内存满了怎么办?
由此我们就可以推断出,它们在**
触发条件
和目标
**上,存在区别。
|------|-----------------|---------------------|
| | 过期删除策略 | 内存淘汰策略 |
| 触发条件 | 键的过期时间到达(TTL到期) | 内存使用达到 maxmemory 限制 |
| 目标 | 清理明确声明不再需要的数据 | 腾出内存空间以维持服务可用性 |
2 内存淘汰策略
🤔 我们刚说到,内存淘汰策略是解决内存满了怎么办?Redis可以存多少数据?什么时候算满?
-
在32位操作系统中,使用
maxmemory
来设置最大运行内存,默认值是3G,因为32位的机器最大只支持4GB的内存,而系统本身就需要一定的内存资源来支持运行,默认3G相对合理。 -
在64位操作系统中,
maxmemory
的默认值是0,表示没有内存大小限制,也可以主动配置maxmemory
-
当Redis存储超过这个配置值,则触发内存淘汰,所以说,内存满了其实就是达到设置的maxmemory值了
2.1 有哪些内存淘汰策略?

2.1.1 不进行数据淘汰的策略
noeviction(Redis3.0之后,默认的内存淘汰策略):它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
2.1.2 进行数据淘汰的策略
2.1.2.1 在设置了过期时间的数据中进行淘汰
-
volatile-random:随机淘汰设置了过期时间的任意键值
-
volatile-ttl:优先淘汰更早过期的键值
-
volatile-lru(Redis3.0之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值
-
volatile-lfu(Redis4.0后新增的内存淘汰策略):淘汰了所有设置过期时间的键值中,最少使用的键值。
2.1.2.2 在所有数据范围内进行淘汰
-
allkeys-random:随机淘汰任意键值
-
allkeys-lru:淘汰整个键值中最久未使用的键值
-
allkeys-lfu(Redis4.0后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
2.1.3 内存淘汰算法LRU
🤔 什么是LRU算法?
LRU全称是Least Recently Used,翻译为**最近最少使用
**,会选择淘汰最近最少使用的数据。
传统LRU算法的实现是基于"链表"结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素
Redis并没有使用这样的方式实现LRU算法,因为传统的LRU算法存在两个问题:
-
需要用链表管理所有的缓存数据,这会带来额外的空间开销
-
当用数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低Redis缓存性能
Redis是如何实现LRU算法的?
-
Redis实现的是一种
近似LRU算法
,目的是为了更好的节约内存,它的实现方式是在R
edis的对象结构体中添加一个额外的字段lru
,用于记录此数据的最后一次访问时间
。 -
当Redis进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取5个值(这个值可以进行配置),然后淘汰最久没有使用的哪个
-
Redis实现LRU算法的优点:
-
不用为所有的数据维护一个大链表,节约了空间占用
-
不用在每次数据访问时都移动链表项,提升了缓存的性能
-
-
但是LRU算法有一个问题:无法解决缓存污染问题:
-
当应用一次性加载大量仅访问一次的数据时:
-
这些数据的"最后一次访问时间"非常新,会挤占缓存空间
-
即使他们是"一次性"的,LRU也会认为它们"最近被使用过",而淘汰真正有价值但最后一次访问较早的热点数据
-
即短期批量操作干扰长期热点数据的保留
-
-
就像你学习了一天,刚刚打开手机看了一眼,你的家长回家,说,怎么就知道玩手机,不学习
补充:Redis的对象结构体
Redis中的key和value都被封装成redisObject结构体,key的类型只能是字符串类型,而value的类型可以是任意的Redis数据类型。
cpp
typedef struct redisObject {
unsigned type:4; // 对象类型(如字符串、哈希等)
unsigned encoding:4; // 对象编码(底层实现方式)
unsigned lru:LRU_BITS; // LRU时间戳 或 LFU计数器(内存淘汰策略相关)24bit
int refcount; // 引用计数器(内存回收)
void *ptr; // 指向实际数据的指针
} robj;
🤔 如何解决缓存污染的问题呢?
Redis4.0之后引入了LFU算法来解决这个问题。
2.1.4 内存淘汰算法LFU
LFU全称是Least Frequently Used翻译为**最近最不常用
**,LFU是根据数据访问次数来淘汰数据的,它的核心思想是"如果数据过去被访问多次,那么将来被访问的频率也更高"。
所以LFU算法会记录每个数据的访问次数
。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题
Redis是如何实现LFU算法的?
-
LFU算法相比于LRU算法的实现,多记录了
数据的访问频次
的信息。 -
LRU和LFU并不会同时开启,基于这个情况,加上节约内存的考虑,Redis在LFU策略下复用
lru字段
,用它来表示LFU的信息 -
将24bits的lru字段分成两段来存储,高16bit存储ldt(Last Decrement Time),低8bit存储logc(Logitstic Counter)
-
ldt是用来记录key的访问时间戳-
logc是用来记录key的访问频次,它的值越小表示使用的频率越低,越容易淘汰,每个新加入的key的logc初始值为5
-
注:logc并不是单纯的访问次数,而是访问频次(访问频率),因为logc会随时间推移而衰减。
- 如果上一次访问时间很久,那么访问频次就会衰减,比如一个key,它原来的logc是255,夸张一点,一年没访问了,不该衰减吗
-
Redis访问key时,logc的变化:
先按照上次访问距离当前时长,来对logc进行衰减
然后,再按照一定概率增加logc的值
redis.conf
提供了两个配置项,用于调整LFU算法从而控制logc的增长和衰减:
lfu-decay-time
用于调整logc的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time值越大,衰减越慢
lfu-log-factor
用于调整logc的增长速度,lfu-log-factor值越大,logc增长越慢