Redis的数据过期清除策略 与 内存淘汰策略

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis的数据过期清除策略 与 内存淘汰策略

在使用Redis时,我们一般会为Redis的缓存空间设置一个大小,不会让数据无限制地放入Redis缓存中。可以使用下面命令来设定缓存的大小,比如设置为4GB:

CONFIG SET maxmemory 4gb

既然 Redis 设置了缓存的容量大小,那缓存被写满就是不可避免的。当缓存被写满时,我们需要考虑下面两个问题:决定淘汰哪些数据,如何处理那些被淘汰的数据。

一、Redis的数据过期清除策略:

如果我们设置了Redis的key-value的过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用 定期删除 + 惰性删除 两种数据过期清除策略。

1、过期策略:定期删除+惰性删除:

(1)定期删除:redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。

为什么不用定时删除策略呢?

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。

(2)惰性删除:定期删除可能导致很多过期的key 到了时间并没有被删除掉。这时就要使用到惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且过期了,是的话就删除。

2、定期删除+惰性删除存在的问题:

如果某个key过期后,定期删除没删除成功,然后也没再次去请求key,也就是说惰性删除也没生效。这时,如果大量过期的key堆积在内存中,redis的内存会越来越高,导致redis的内存块耗尽。那么就应该采用内存淘汰机制。

二、Redis的缓存淘汰策略:

Redis共提供了8中缓存淘汰策略,其中 volatile-lfu 和 allkeys-lfu 是Redis 4.0版本新增的。

image

1、noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。Redis 用作缓存时,实际的数据集通常都是大于缓存容量的,总会有新的数据要写入缓存,这个策略本身不淘汰数据,也就不会腾出新的缓存空间,我们不把它用在 Redis 缓存中。

2、volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。

3、volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。

4、volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。

5、volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对

6、allkeys-random:在所有键值对中,随机移除某个key。

7、allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。

8、allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对

通常情况下推荐优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。

如果你的业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。

如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。

如果没有设置过期时间的键值对,那么 volatile-lru,volatile-lfu,volatile-random 和 volatile-ttl 策略的行为, 和 noeviction 基本上一致。

三、Redis中的LRU和LFU算法:

1、LRU算法:

LRU 算法的全称是 Least Recently Uses,按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来。LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。我们看一个例子。

image

如果有一个新数据 45 要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么LRU 算法做两件事:数据 45 是刚被访问的,所以它会被放到 MRU 端;算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。LRU认为刚刚被访问的数据,肯定还会被再次访问,所以就把它放在 MRU 端;长久不访问的数据,肯定就不会再被访问了,所以就让它逐渐后移到 LRU 端,在缓存满时,就优先删除它。

LRU 算法在实际实现时,需要用链表管理所有的缓存数据,移除元素时直接从链表队尾移除,增加时加到头部就可以了,但这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

所以,在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。具体来说:Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这里的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 N 个,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。

Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。例如,我们执行如下命令,可以让 Redis 选出 100 个数据作为候选数据集:

CONFIG SET maxmemory-samples 100

RedisObject 的定义如下:(简单理解为一个 key-value)

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

2、LFU算法:

LFU是在Redis4.0后出现的,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。它的使用与LRU有所区别:

LFU (Least Frequently Used) :最近最不频繁使用,跟使用的次数有关,淘汰使用次数最少的。

LRU (Least Recently Used):最近最少使用,跟使用的最后一次时间有关,淘汰最近使用时间离现在最久的。

LRU的最近最少使用实际上并不精确,考虑下面的情况,如果在 “|” 处删除,那么A距离的时间最久,但实际上A的使用频率要比D频繁,所以合理的淘汰策略应该是淘汰D。LFU就是为应对这种情况而生的。

~~~A~A~AA~A~A|

RRRRRRRRRRRR~|

~~~~C~C~C~~~~|

~~~D~~D~D~~~D|

每个波浪号代表一秒,A 每五秒,R 每两秒,C 和 D 每十秒 , 最近被访问的字符是 D,但显然按照现有的规律,下一个被访问的更可能是 R 而不是 D。

LFU 实现比较复杂,需要考虑几个问题:

如果实现为链表,当对象被访问时按访问次数移动到链表的某个有序位置可能是低效的,因为可能存在大量访问次数相同的 key,最差情况是O(n) 
某些 key 访问次数可能非常之大,理论上可以无限大,但实际上我们并不需要精确的访问次数
访问次数特别大的 key 可能以后都不再访问了,但是因为访问次数大而一直占用着内存不被淘汰,需要一个方法来逐步“驱除”(有点 LRU的意思),最简单的就是逐步衰减访问次数

本着能省则省的原则,Redis 只用了 24bit (server.lruclock 也是24bit)来记录上述的信息,是的不是 24byte,连32位指针都放不下!

16bit : 上一次递减时间 (解决第三个问题)

8bit : 访问次数 (解决第二个问题)

访问次数的计算如下:

uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;
    double r = (double)rand()/RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r < p) counter++;
    return counter;
}

核心就是访问次数越大,访问次数被递增的可能性越小,最大 255,可以在配置 redis.conf 中写明访问多少次递增多少。由于访问次数是有限的,所以第一个问题也被解决了,直接一个255数组或链表都可以。

16bit 部分保存的是时间戳的后16位(分钟),表示上一次递减的时间,算法是这样执行,随机采样N个key,检查递减时间,如果距离现在超过 N 分钟(可配置),则递减或者减半(如果访问次数数值比较大)。

此外,由于新加入的 key 访问次数很可能比不被访问的老 key小,为了不被马上淘汰,新key访问次数设为 5

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
28天前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
19天前
|
NoSQL 算法 Redis
redis内存淘汰策略
Redis支持8种内存淘汰策略,包括noeviction、volatile-ttl、allkeys-random、volatile-random、allkeys-lru、volatile-lru、allkeys-lfu和volatile-lfu。这些策略分别针对所有键或仅设置TTL的键,采用随机、LRU(最近最久未使用)或LFU(最少频率使用)等算法进行淘汰。
35 5
|
22天前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
54 7
|
1月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
60 11
|
23天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
54 1
|
1月前
|
存储 分布式计算 算法
1GB内存挑战:高效处理40亿QQ号的策略
在面对如何处理40亿个QQ号仅用1GB内存的难题时,我们需要采用一些高效的数据结构和算法来优化内存使用。这个问题涉及到数据存储、查询和处理等多个方面,本文将分享一些实用的技术策略,帮助你在有限的内存资源下处理大规模数据集。
32 1
|
1月前
|
存储 监控 Java
深入理解计算机内存管理:优化策略与实践
深入理解计算机内存管理:优化策略与实践
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
104 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。