使用Redis缓存时,您可以为缓存数据设置过期时间,提高缓存命中率,并根据过期策略删除缓存数据。
Redis 的缓存过期删除策略有两种类型:延迟删除和定期删除。
在 Redis 中,设置了过期时间的密钥将存储在 Redis 的过期字典中。 当你对密钥进行读写操作时,会先检查过期的字典中是否存在密钥,如果存在,则获取密钥的过期时间,并与系统当前时间进行对比
如果过期时间大于当前时间,则密钥尚未过期。
如果过期时间小于或等于当前时间,则确定密钥已过期。
过时的词典
过期的字典存储在 RedisDB 结构中,如下所示:
typedef struct redisdb redisdb;哪里:
过期字典的键指向 Redis 数据库中的键。
过期字典的值为long long类型的整数,用于存储密钥对应的过期时间(timestamp,单位:毫秒)。
如图所示
延迟删除处理逻辑,如图所示
过程:对密钥执行读写操作,以确定过期的字典中是否存在该密钥
如果不存在,则正常执行读写操作。
如果是这样,请将密钥过期时间与系统的当前时间进行比较,以确定密钥是否已过期
如果密钥的过期时间小于或等于当前时间(过期),则删除该密钥。
如果密钥过期时间长于当前时间(未过期),系统将执行读/写操作。
定期删除处理逻辑,如图所示
操作步骤:Redis以100ms的频率从过期的字典中随机抽取20个密钥,检查是否有过期的密钥
如果密钥的过期时间小于或等于当前时间,则表示密钥已过期。
如果密钥过期时间大于当前时间,则密钥尚未过期。
如果存在过期密钥,请确定过期密钥数量是否超过提取数量的 25%,或者周期性删除周期是否超过 25 毫秒
如果执行周期超过25%或执行时长超过25ms,则从过期字典中随机抽取20个密钥进行检查,循环至过期密钥数量不超过25%或执行时长不超过25ms。
如果执行时间不超过 25 毫秒,则删除过期的密钥。
如果没有过期密钥,请等待 100 毫秒,然后继续从过期的字典中随机选择 20 个密钥进行检查。
注意:Redis在主线程上会定期删除,如果大量密钥同时过期,会影响Redis的读写效率。
redis2.在版本 8 之后,可以通过修改配置文件来重新开发conf 中的 hz 参数(默认值为 10,即每秒 10 次),用于调整定时删除的定时任务的执行频率。
参数 Hz:Hz 的值范围为 1 500,默认值为 10,不建议超过 100,增加 Hz 的值会占用更多的 CPU 资源,只有当请求延迟很低时,才能考虑将 Hz 的值增加到 100。
Redis 过期删除策略采用延迟删除 + 定期删除作为 CPU 资源和内存消耗之间的权衡
如果使用延迟删除,未访问的密钥将存储在内存中,从而造成内存资源的浪费。
如果只使用周期性删除,当大量密钥同时过期时,会占用大量的CPU资源,影响读写操作的性能和系统吞吐量。
过期删除策略是一种不精确的删除策略,对于那些长时间未访问且多次定期删除的密钥,它们将始终存储在内存中,这可能会导致 Redis 的内存耗尽。
在配置文件redis中conf,最大内存由参数 maxmemory 设置(默认为 unlimited,该参数的值通常设置为物理内存的 3 4)。
当Redis内存超过maxmemory参数设置的最大内存时,触发内存消除策略。 内存消除策略是使用 maxmemory-policy 参数设置的,如下所示:
volatile-lru:设置了过期时间的密钥,通过LRU算法淘汰(lru:最近最少使用,表示使用时间最长)。
allkeys-lru:根据 LRU 算法(环境中使用的常见策略)消除所有密钥(包括有和没有过期时间的密钥)。
volatile-lfu:根据 LFU 算法淘汰有过期时间的密钥(lfu:最不常用)。
allkeys-LFU:基于LFU算法消除所有密钥。
volatile-random:设置了过期时间的密钥,通过随机消除方法进行淘汰。
allkeys-random:根据随机消除方法消除所有键。
volatile-ttl:对于设置了过期时间的密钥,丢弃即将过期的密钥(次要 TTL)。
noeviction:不排除任何数据,当使用的内存空间超过maxmemory参数设置的值时,写入数据到Redis时会返回错误(默认策略,一般不使用)。
可以看出,除了特殊的noeviction和volatile-TTL策略外,其他六种策略可以分为两类:
从 volatile- 开始的策略是清除根据特定算法设置了过期时间的密钥。
从 allkeys- 开始的策略是根据特定算法清除所有键。
redis.conf 中与内存消除策略相关的参数:
maxmemory:内存使用上限,默认值为0,表示内存使用上限不受限制。
maxmemory-policy:内存消除策略,默认策略为 noeviction。
maxmemory-samples:随机样本数,默认值为 5。
内存停用策略选择:
如果数据是幂等的(即某些数据的访问频率更高,而其余数据的访问频率较低),建议您选择 allkeys-lru 或 allkeys-lfu 策略。
如果数据是均匀分布的(即所有数据的访问概率大致相等),建议您选择 allkeys-random 策略。
如果您需要设置不同的TTLS来确定数据过期的顺序,建议您选择Volatile-TTL策略。
如果有些数据需要长时间存储,有些数据可以清除,建议选择 volatile-lru 或 volatile-random 策略。
注意:由于设置过期时间会消耗额外的内存,因此您可以选择 allkeys-lru 策略,以防止 Redis 因确定密钥的过期时间而消耗额外的内存。
要查看 Redis 使用的内存停用策略,请参阅 config get maxmemory-policy。
修改内存消除策略
config set maxmemory-policy Policy:立即生效,重启并恢复为默认值。
修改配置文件 Redisconf Policy中的参数maxmemory-policy:重启生效。
redis - 大致的 LRU 计算执行过程:
随机选择n个key(n由参数maxmemory-samples设置,默认值为5),找到时间最长的未访问密钥(时间通过比较RedisObject中的LRU属性与系统当前时间确定),并删除该密钥。
如果系统内存再次超过内存使用限制,并且仍然超过内存使用限制,则上述过程将继续。
LRU 算法中的缺陷
LRU算法只关注数据的访问时间或顺序,忽略了访问次数的值,在剔除数据的过程中可能会剔除热点数据。
如图所示
图中时间线从左到右,数据a b c在同一时间段内被多次访问。 数据C为最近访问的数据,按照LRU算法排列的数据热度为C>B>A,数据实热度为B>A>C。
为什么 Redis 不只使用 LRU 算法?
如果使用 LRU 算法(基于 hashmap + 双链表:稍后将共享一个 LRU 算法),则需要消耗额外的内存来存储双链表节点中的上一个和下一个指针,从而减少 Redis 的存储空间。
为什么maxmemory-samples参数的默认值为5?
当提取次数为 5 时,执行效率非常接近标准 LRU 算法,虽然提取次数设置为 5 时可以更接近 LRU 算法,但会消耗更多的 CPU 资源,因此提取次数的默认值是 LRU 算法的执行效率和 CPU 资源消耗权衡的结果。
LFU(full name least used used)代表最近使用频率最低的,与关键访问次数有关,其核心思想是,如果一个数据在不久的将来被频繁访问,那么未来被重新访问的概率会很高,访问频率较低的数据将来不会再次使用的概率会很高。
注意:访问LFU算法的频率不能等同于访问次数。 如图所示
图中,数据 A 被访问了 5 次,数据 B 和 C 各被访问了 4 次
如果数据热度值由访问次数决定,则该值为 a>b=c。
如果考虑到时效性,更接近当前时间的访问更有价值,则数据热度值为:c>b>a。
因此,LFU算法一般具有时间衰减函数来参与热值的计算,同时考虑接入时间的影响。
LFU算法实现
LFU 算法的实现不使用额外的数据结构,而是重用 RedisObject 数据结构的 LRU 字段。
redisobject 数据结构:
typedef struct redisobject robj;其中,LRU算法和LFU算法在LRU字段的使用方式存在差异
在LRU算法中,LRU字段用于记录密钥的访问时间戳,因此可以根据LRU字段中记录的值,将长时间未访问的密钥与当前时间进行比对。
在 LFU 算法中,LRU 字段分为两段,最高 16 位存储 LDT(最后递减时间)和最低 8 位存储逻辑计数器 (LOGC)。
LFU算法中的LRU字段,如图所示:
ldt:密钥的访问时间戳。
logc:用于记录密钥的访问频率(即流行度值),logc值越小,访问频率越低,被淘汰的可能性越大,新密钥的logc初始值为5(目的是防止新密钥立即被淘汰, 并且对新密钥的访问次数可能小于未访问的旧密钥的访问次数)。
LFU接入频率(逻辑计数器)算法实现:
#define lfu_init_val 5/* logarithmically increment a counter. the greater is the current counter value * the less likely is that it gets really implemented. saturate it at 255. */uint8_t lfulogincr(uint8_t counter)哪里:
如果计数器小于或等于 LFU init val,则一旦访问并命中数据,计数器的概率接近 100%,并增加 1。
如果计数器大于 lfu init val,则需要先计算两者之间的差值,然后作为分母的一部分参与计算递增概率。
随着计数器值的增加,增加的概率逐渐衰减,并且多次访问可能不会将其值增加 1。
当计数器值达到 255 时,不再执行值递增过程。
为了适应各种业务数据的特点,Redis 在 LFU 算法的实现中引入了两个可调参数:
lfu-decay-time:时间衰减因子,单位为分钟,默认值为1,该参数值越高,衰减越慢。
lfu-log-factor:递增衰减因子,默认值为10,该参数值越高,递增概率越低。
阅读推荐]更多精彩内容,如:
Redis 系列。
数据结构和算法。
NACOS系列。
MySQL系列。
JVM 系列。
卡夫卡系列。
并发编程系列。
请移至【南秋】个人主页参考。 内容不断更新。
关于作者]热爱科技、热爱生活的老宝贝,专注J**A领域,关注【南秋同学】带你一起学习、共同成长