Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
字典实际上是哈希表,哈希表的最大好处就是让我们可以用 O(1)
的时间复杂度来快速查找。当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
定时删除策略(TTL)的做法是,在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
惰性删除策略(Lazy Expire)的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
定期删除策略(Eviction)的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
过期删除策略 | 删除时间 | CPU 资源消耗 | 内存开销 |
---|---|---|---|
TTL(定时删除) | 立即删除 | 较高 | 无 |
Lazy Expire(惰性删除) | 访问时检查删除 | 较低 | 可能有过期键在内存中存留 |
Eviction(定期删除) | 定期抽取删除 | 低 | 可能有过期键在内存中存留 |
Redis使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用,以求在合理使用CPU时间和避免内存浪费之间取得平衡。
Redis 的惰性删除策略由 db.c
文件中的 expireIfNeeded
函数实现,Redis 在访问或者修改 key 之前,都会调用 `expireIfNeeded 函数对其进行检查,检查 key 是否过期:
lazyfree_lazy_expire
参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端;再回忆一下,定期删除策略的做法:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
在 Redis 中,默认每秒进行10次过期检查一次数据库,此配置可通过 Redis 的配置文件 redis.conf
进行配置,配置键为 hz
,它的默认值是 hz 10
。
值得注意的是,每次检查数据库并不是遍历过期字典中的所有 key,而是从数据库中随机抽取一定数量的 key 进行过期检查。这个一定数量在源码中是写死的,并未提供对应的参数进行自定义配置,数值固定为20。
Redis 定期删除的流程为:
20
个 key;20
个 key 是否过期,并删除已过期的 key;超过5个(20*0.25)
,也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%
,则继续重复步骤 1,即重复执行删除策略;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过
25ms
。
Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File),下面我们分别来看过期键在这两种格式中的呈现状态。
RDB
文件分为两个阶段,RDB 文件生成阶段和加载阶段:
AOF
文件分为两个阶段,AOF 文件写入阶段和重写阶段。
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis的内存淘汰机制是为了解决内存占用过高的问题。在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,根据一定的策略来选择一些键值对进行删除,从而释放部分内存。这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory
。
常见的内存淘汰策略有:
LRU(Least Recently Used,最近最少使用)
:淘汰整个键值中最久未使用的键值;LFU(Least Frequently Used,最不经常使用)
:淘汰整个键值中最少使用的键值;Random(随机)
:随机选择键值对进行淘汰。noeviction(不进行数据淘汰)
:Redis3.0之后,默认的内存淘汰策略。它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。虽然虽然,但是后面就不介绍后两个策略了,主要介绍前两个策略:
- 随机策略:想介绍也没东西介绍,就随缘抓几个起来噶掉这玩意
- 不进行数据淘汰策略:想介绍也没东西介绍,就直接把门关了这玩意
LRU(Least Recently Used,最近最少使用)是Redis3.0之前默认的内存淘汰策略,它是淘汰整个键值中最久未使用的键值。
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是默认随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
但是 LRU 算法有一个问题,由于是随机采样的方式来淘汰数据,因此无法解决缓存污染问题。比如应用一次读取了大量的数据,而这些数据只会被读取这一次,如果运气炸裂每次随机采样都采不到它们,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,是在Redis4.0新增的一种内存淘汰策略。
LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
其实严格来说,LFU算法是根据数据访问频率来淘汰数据的。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。Redis 对象的结构如下:
typedef struct redisObject {
...
// 24 bits,用于记录对象的访问信息
unsigned lru:24;
...
} robj;
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。
在 LRU 算法中,Redis 对象头的 24 bits
的 lru
字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru
字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
在 LFU 算法中,Redis对象头的 24 bits
的 lru
字段被分成两段来存储,高16bit
存储 ldt
(Last Decrement Time),低8bit
存储 logc
(Logistic Counter):
ldt
是用来记录 key 的访问时间戳;logc
是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的logc 初始值为 5。注意,
logc
并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的。
在每次 key 被访问时,会先对 logc
做一个衰减操作,衰减的值跟前后访问时间的差距有关系.如果上一次访问的时间与这一次访问的时间差距很大,那么衰减的值就越大,这样实现的 LFU 算法是根据访问频率来淘汰数据的,而不只是访问次数。
访问频率需要考虑 key 的访问是多长时间段内发生的。key 的先前访问距离当前时间越长,那么这个 key 的访问频率相应地也就会降低,这样被淘汰的概率也会更大。
对 logc
做完衰减操作后,就开始对 logc
进行增加操作,增加操作并不是单纯的自增,而是根据概率增加,如果 logc
越大的 key,它的 logc
就越难再增加。
所以,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
增长越慢。策略 | LRU(最近最少使用) | LFU(最不经常使用) |
---|---|---|
淘汰原则 | 最久未使用的键值对 | 访问次数最少的键值对 |
更新机制 | 当键被访问时更新使用时间 | 当键被访问时增加访问次数 |
适用场景 | 处理热数据,最近被访问频繁的数据 | 处理稳定数据,访问次数相对固定的数据 |
内存淘汰机制:
过期删除机制:
特点 | 内存淘汰机制 | 过期删除机制 |
---|---|---|
目的 | 释放内存空间 | 维护数据的时效性 |
触发时机 | 当 Redis 内存达到上限时,触发淘汰策略 | 当键设置了过期时间后,等待键过期触发过期删除策略 |
依赖键的过期时间 | 不依赖键的过期时间,所有键都有可能被淘汰 | 只对设置了过期时间的键起作用 |
策略 | 1. LRU 2. LFU 3. 随机 |
1. 定时删除 2. 惰性删除 3. 定期删除 |