redis

Redis过期策略和内存淘汰机制

2022-11-26  本文已影响0人  小波同学

一、关于Redis内存回收

Redis是基于内存操作的非关系型数据库,Redis中提供了多种内存回收策略,当内存容量不足时,为了保证程序的运行,这时就不得不淘汰内存中的一些对象,释放这些对象占用的空间,那么选择淘汰哪些对象呢?

Redis的内存回收,主要围绕以下两种方式:

注意:过期策略和淘汰策略是两种不同的概念。

二、Redis过期策略

在Redis中,提供了expire命令设置一个键的过期时间,到期之后Redis会自动删除它,这个在我们的实际使用过程中用的非常多。

Redis中设置过期时间有如下两种方式:

expire命令的seconds单位为秒,最小精确至1秒,如果想要更精确的控制键的过期时间,可以使用pexpire命令,pexpire命令的单位是毫秒。pexpire key 1000与expire key 1相等。

Redis 过期时间相关命令

1、设置过期时间

Redis 提供了四个命令来设置过期时间(生存时间):

在Redis内部实现中,前面三个设置过期时间的命令最后都会转换成最后一个PEXPIREAT 命令来完成。

2、移除过期时间

3、查看键的剩余过期时间

三种过期策略

Redis采用的过期策略:惰性删除+定期删除。

1、Redis定期删除策略

redis.c/activeExpireCycle函数实现,函数以一定频率执行,每当Redis的服务器性执行redis.c/serverCron函数时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键 。

为什不扫描所有的 key?

  • Redis 是单线程,全部扫描岂不是卡死了。而且为了防止每次扫描过期的 key 比例都超过 1/4,导致不停循环卡死线程,Redis 为每次扫描添加了上限时间,默认是 25ms。
    如果在同一时间出现大面积 key 过期,Redis 循环多次扫描过期词典,直到过期的 key 比例小于 1/4。这会导致卡顿,而且在高并发的情况下,可能会导致缓存雪崩。
从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。

注意

...
# The range is tetween 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is requiried.
hz 10
...

在这个参数的上面注释可以看出,建议不要将这个值设置超过100,一般使用默认的10,只有当在需要非常低延迟的场景才设置为100。

2、Redis惰性删除策略

Redis的惰性删除策略由db.c/expireIfNeeded函数实现,所有键读写命令执行之前都会调用expireIfNeeded函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。

3、Redis 过期删除策略的问题

虽然 Redis 采用了惰性删除和定期删除两种策略,但对于一些永远使用不到的键,并且经过多次定期删除也没有被选定到并删除,那么这些键就会一直驻留在内存中。

所以,这时候就需要使用 Redis 的内存淘汰策略来解决了。

三、Redis 内存淘汰策略

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
maxmemory <bytes>

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> remove the key with an expire set using an LRU algorithm
# allkeys-lru -> remove any key according to the LRU algorithm
# volatile-random -> remove a random key with an expire set
# allkeys-random -> remove a random key, any key
# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# noeviction -> don't expire at all, just return an error on write operations

Redis可以设置内存大小:maxmemory 100mb,超过了这个内存大小,就会触发内存淘汰机制;

配置:在redis.conf 配置文件中,可以设置淘汰方式:maxmemory-policy noeviction

Redis 4.0开始,共有8种数据淘汰机制:见Redis的配置文件redis.conf:


针对设置过期时间的键值对:即使缓存没有写满,这些数据如果过期了,也会被删除。
除。当然,如果它的过期时间到了但未被策略选中,同样也会被删除。

设置过期时间的键值对

所有键值对

除 noeviction 比较特殊外,allkeys 开头的将从所有数据中进行淘汰,volatile 开头的将从设置了过期时间的数据中进行淘汰。淘汰算法又核心分为 lru、random、ttl、lfu 几种。如下图所示:

结和归纳

总体来说,可以从2个维度,四个方面来个8中淘汰策略分类:

生产环境中,如何配置缓存淘汰策略

maxmemory-policy allkeys-lru
## 设置内存数据的淘汰策略为volatile-lru
 config set maxmemory-policy volatile-lru
 config get maxmemory-policy

四、Redis的LRU、LFU算法

1、LRU算法

LRU(Least Recently Used)最近最少使用。优先淘汰最近未被使用的数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。LRU底层结构是 hash 表 + 双向链表。hash 表用于保证查询操作的时间复杂度是O(1),双向链表用于保证节点插入、节点删除的时间复杂度是O(1)。

为什么是 双向链表而不是单链表呢?单链表可以实现头部插入新节点、尾部删除旧节点的时间复杂度都是O(1),但是对于中间节点时间复杂度是O(n),因为对于中间节点c,我们需要将该节点c移动到头部,此时只知道它的下一个节点,要知道其上一个节点需要遍历整个链表,时间复杂度为O(n)。

LRU GET操作:如果节点存在,则将该节点移动到链表头部,并返回节点值;

LRU PUT操作:①节点不存在,则新增节点,并将该节点放到链表头部;②节点存在,则更新节点,并将该节点放到链表头部。

【LRU缓存】【hash+双向链表】结构示意图如下:


2、近似LRU算法原理(approximated LRU algorithm)

Redis为什么不使用原生LRU算法?

在Redis中,Redis的key的底层结构是 redisObject,redisObject 中 lru: LRU_BITS 字段用于记录该key最近一次被访问时的Redis时钟 server.lruclock(Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟)。不太理解Redis时钟的,可以将其先简单理解成时间戳(不影响我们理解近似LRU算法原理。

# Redis的key的底层结构,源码位于:server.h
 
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; // 指向存储实际值的数据结构的指针,数据结构由 type、encoding 决定。
} robj;

当 mem_used > maxmemory 时,Redis通过 freeMemoryIfNeeded 方法完成数据淘汰。LRU策略淘汰核心逻辑在 evictionPoolPopulate(淘汰数据集合填充) 方法。

Redis 近似LRU 淘汰策略逻辑:

3、LFU算法原理

LFU 使用 Morris counter 概率计数器,仅使用几比特就可以维护 访问频率,Morris算法利用随机算法来增加计数,在 Morris 算法中,计数不是真实的计数,它代表的是实际计数的量级。
LFU数据淘汰策略下,redisObject 的 lru:LRU_BITS 字段(24位)将分为2部分存储:

注意:

Redis的LFU算法

4、LFU与LRU的区别

如果一条数据仅仅是突然被访问(有可能后续将不再访问),在 LRU 算法下,此数据将被定义为热数据,最晚被淘汰。但实际生产环境下,我们很多时候需要计算的是一段时间下key的访问频率,淘汰此时间段内的冷数据。LFU 算法相比 LRU,在某些情况下可以提升 数据命中率,使用频率更多的数据将更容易被保留。

对比项 近似LRU算法 LFU算法
最先过期的数据 最近未被访问的 最近一段时间访问的最少的
适用场景 数据被连续访问场景 数据在一段时间内被连续访问
缺点 新增key将占据缓存 历史访问次数超大的key淘汰速度取决于lfu-decay-time

淘汰策略使用

缓存污染

在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。

当缓存污染不严重时,只有少量数据占据缓存空间,此时,对缓存系统的影响不大。但是,缓
https://blog.csdn.net/Extraordinarylife/article/details/127344560存污染一旦变得严重后,就会有大量不再访问的数据滞留在缓存中。如果这时数据占满了缓存空间,我们再往缓存中写入新数据时,就需要先把这些数据逐步淘汰出缓存,这就会引入额外的操作时间开销,进而会影响应用的性能。

要解决缓存污染问题,最关键的技术点就是能识别出这些只访问一次或是访问次数很少的数据,在淘汰数据时,优先把它们筛选出来并淘汰掉。所以采用LFU策略

参考:
https://blog.csdn.net/Extraordinarylife/article/details/127344560

https://blog.nowcoder.net/n/e7f3994adb62441ca40041f6854b3cb9

https://blog.csdn.net/FlyingFish868/article/details/125964074

https://blog.csdn.net/weixin_43863054/article/details/126243012

https://blog.csdn.net/aiwangtingyun/article/details/123995143

https://blog.csdn.net/DQWERww/article/details/126453008

https://blog.csdn.net/weixin_38871362/article/details/125678494

上一篇下一篇

猜你喜欢

热点阅读