redis内存回收与内存淘汰策略

给新观众老爷的开场

大家好,我是弟弟!
最近读了一遍 黄健宏大佬的 <<Redis 设计与实现>>,对Redis 3.0版本有了一些认识
该书作者有一版添加了注释的 redis 3.0源码
👉官方redis的github传送门
👉黄健宏大佬添加了注释的 redis 3.0源码传送门
👉antirez的博客

网上说Redis代码写得很好,为了加深印象和学习redis大佬的代码写作艺术,了解工作中使用的redis 命令背后的源码逻辑,便有了写博客记录学习redis源码过程的想法。

redis内存回收

日常对redis的使用,除了很少的k/v可以不设置过期时间以外,绝大多数k/v都是带过期时间的。
这样当key过期之后redis可以将k/v删除,以释放出内存来存储其他的k/v。

redis作为一个内存型数据库,所有的数据都放在内存里。
内存相对硬盘来说还是比较贵的,内存资源比较少。
对内存的使用能省就省,能回收就回收,提高一些内存资源的利用率。

内存回收的方式

对过期k/v的回收方式有三种,分别是定时删除,惰性删除,定期删除。
redis采用的是 惰性删除+定期删除 两种策略。

1. 定时删除

比如一个key的过期时间是1小时,那么设置一个1小时后执行的定时器来定时删除这个key。
这个方式乍一看上去是内存回收效率最高的方式,但redis因为主要工作线程是一个单线程的缘故,对cpu的计算耗时非常的敏感
每个key一个定时器的话,100万个带过期时间的k/v就有100万个定时器,会极其消耗cpu,从而影响redis性能。

该单线程工作的cpu被阻塞几十毫秒会造成这期间所有待处理的请求延迟增加几十毫秒以上
对于redis这种内存型数据库来说,请求延时超过10ms以上都算慢的了。

2. 惰性删除

既然定时删除的定时器消耗cpu,那干脆对有过期时间的key不加定时器了。这样cpu也没有额外的负担,当过期的key再次被访问时,再检查key的过期时间是否过期,如果过期就删除掉。
但这样的缺点也比较明显,redis中会存在大量过期而又没被删除的key,内存回收效率不高。

3. 定期删除

这种方式算是定时删除 与 惰性删除的一种折中方式。
定期删除每隔一段时间处理部分过期key,
通过少量多次的方式 来提高内存回收效率,
同时将cpu消耗分散到了每一次操作上,限制每次定期删除执行的最大时间和处理的k/v个数,避免了因单次重度的cpu计算消耗,影响线上正常请求。

  1. 通过配置文件中的 hz 字段来指定 serverCron 1秒运行的次数。
    再每次serverCron中 会检查部分具有过期时间戳的key,如果过期,则将key删除。

    默认 hz 10,也就是1秒执行10次serverCron

  2. 在每次事件轮询前执行的beforeSleep函数中,会检查部分具有过期时间戳的key,如果过期,则将key删除。

    定期删除的策略,一定程度上弥补了惰性删除的 回收效率不高的问题,也一定程度上避免了定时删除的高cpu消耗的问题。通过少量多次的方式,可以在整体上达到与完美内存回收差不多的效果。

    可以通过调整配置文件中的 hz 的大小,来调整过期key回收的频率。
    较大的hz值,意味着较高的过期key回收频率,也意味着增加了较多的cpu消耗。

4. 大key删除

通过惰性删除和定期删除两种方式,发现需要删除的过期k/v时,
在redis 4.0版本之前,会直接将过期的k/v删除。
在删除较小的k/v时问题不大。

但是线上不可避免的会出现value较大的k/v,
比如一个实时排行榜,可能一个zset中就会包含几十万个成员及其分数。
当value较大时,对大块内存的释放操作,将对redis的主要工作的线程产生不小的阻塞。

key/value的长度默认最大都是512mb,不过通常key都是很小的,value可能会比较大。

所以仅通过 惰性删除+定期删除两种方式来删除过期k/v 是还不够的,必须对大key特殊处理。
于是在redis4.0 中加入了 unlink 命令 以及 lazyfree机制

unlink
对需要删除的k/v仅从全局k/v字典中摘除相关的条目,并不立马删除真正的k/v。

lazy free
若 value的相关属性 大于 LAZYFREE_THRESHOLD (默认值 64) 且引用计数为1 , 属于较大的value,
会将该value添加到 全局的 lazy_free任务队列,并由专门进行 lazy free 的线程进行内存释放操作。
对较大value的lazy free不会阻塞主线程。

相关属性如 value 长度/元素个数/占用字节数 根据不同数据类型取不同字段
当然key是在主线程被直接释放的,若value较小也是在主线程被直接释放。

redis内存淘汰策略

redis内存淘汰策略有哪些?

虽然可以对具有过期时间的key进行内存回收,
但是内存存储的了未过期的k/v的大小超过了设置的 maxmemory (最大内存使用量),
新的k/v由于没有可用内存而无法写入,
这种情况也是很有可能发生的。
redis提供了几种内存淘汰策略,以及其组合策略 来应对这种情况。

redis的内存淘汰策略如下👇

/* Redis maxmemory strategies. Instead of using just incremental number
 * for this defines, we use a set of flags so that testing for certain
 * properties common to multiple policies is faster. */
'LRU'
#define MAXMEMORY_FLAG_LRU (1<<0)   				 
'LFU'
#define MAXMEMORY_FLAG_LFU (1<<1)   				 
'对有所key进行淘汰'
#define MAXMEMORY_FLAG_ALLKEYS (1<<2)	             

'1. 只对带过期时间的key使用LRU'
#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)   
'2. 只对带过期时间的key使用LFU'
#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)   
'3. 根据key到期时间长短进行淘汰'
#define MAXMEMORY_VOLATILE_TTL (2<<8)						 
'4. 随机淘汰带过期时间的key'
#define MAXMEMORY_VOLATILE_RANDOM (3<<8)			
'5. 对所有key使用LRU'
#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_ALLKEYS)
'6. 对所有key使用LFU'
#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU|MAXMEMORY_FLAG_ALLKEYS)
'7. 随机淘汰所有key'
#define MAXMEMORY_ALLKEYS_RANDOM ((6<<8)|MAXMEMORY_FLAG_ALLKEYS)
'8. 不淘汰key,此策略当内存占满时,几乎大部分写命令都将失败,只能读'
#define MAXMEMORY_NO_EVICTION (7<<8)

'redis的默认内存淘汰策略,不淘汰key'
#define CONFIG_DEFAULT_MAXMEMORY_POLICY MAXMEMORY_NO_EVICTION

从上面redis的源码定义中可以看到,redis的默认内存淘汰策略是不进行淘汰。
此时的观众朋友们是否会感到疑惑?

为什么redis默认不进行内存淘汰

让我们将问题简化一下,来试着分析一下redis默认不进行淘汰的原因。
以下纯属个人分析

假设的场景是这样👇

  1. 假设某个redis服务器实例 最多只能存储 100万个 k/v,且已经存了100万个k/v。
  2. 由于线上环境会持续写入新的k/v,假设每秒新增1万个 k/v 。
  3. 按照上述的内存淘汰策略,在接下来的时间内,
    总是有 1万个 k/v 被淘汰,内存被释放,有1万个k/v会分配到内存并写入。

可能存在的问题👇

  1. 对于缓存类数据的影响
    写入redis中k/v一般都是从db或者其他二级缓存里捞出来放到redis里的,
    这样操作的目的也是希望通过redis缓存来加速数据的访问,
    如果由于有新的缓存数据写入而把之前放到redis中的缓存数据淘汰,
    实际上是在拆东墙补西墙,
    当被删除的缓存数据再次被访问还需要重新在db或二级缓存捞一遍
    这样,redis对整体数据访问的加速可能没有提高
    反而可能因为频繁的释放/分配内存、重新缓存db数据到内存而导致性能降低

    当然内存中存在访问频率低的数据,被淘汰出内存是没有什么问题,但随着新的k/v不断的产生,当访问频率低的数据大部分都被淘汰出去后,就又回到了这个问题。

  2. 对功能类数据的影响
    redis提供了非常易用的一个 set nx 命令,可以用来做分布式锁。
    用来保证在大部分情况下,某些操作只能在规定时间内执行一次。
    如果该类数据因内存淘汰而被删除,将对线上的业务产生不可预估的影响。

综上可以看出,
redis的内存淘汰策略仅仅是作为一个内存不够用时的兜底策略而并非最终解决方案。
单机内存不够用时,可以采用分片的方法,扩大redis的整体容量。

但redis仍然提供了额外的内存淘汰策略,
可通过修改配置文件的maxmemory-policy来满足特殊用途。

redis中的内存淘汰策略,在淘汰内存时需要兼顾 尽量小的内存占用/以及尽量小的cpu资源消耗,故redis中的内存淘汰策略都是 近似&随机 的策略。

redis的LRU内存淘汰策略

LRU (Least Recently Used) 也就是淘汰最近最少使用的k/v,
也是 最久未使用的k/v的意思。

严格的LRU会有什么问题

一种能严格淘汰 最近最少使用的k/v 的实现方式是

  1. 一个哈希表存储所有的k/v
    一个双向链表按k/v的访问时间顺序排序,假设以降序排序
  2. 当新增一个k/v时,将其插入哈希表后,再加入双向链表的头部。
  3. 当一个k/v被访问时,将其移动到双向链表的头部。
  4. 当lru需要淘汰掉若干个k/v时,从链表尾部依次淘汰就可以了。

redis中是有全局哈希表的,但少了一个双向链表。
让我们来分析一下为什么没有这个双向链表。

  1. 首先一份双向链表会占一份空间,假设采用
    ‘对所有key使用LRU’
    #define MAXMEMORY_ALLKEYS_LRU
    的策略,那整个实例如果有几百万k/v,那就得再有几百万的链表节点。
    显然,很占内存。
  2. redis中大部分k/v的访问频率都是很高的,
    大部分的k/v频繁的访问,会让对应的k/v在双向链表上频繁的移动,
    而我们想要淘汰掉的是访问频率不高的数据
    显然多出来的这些热点k/v的频繁移动操作
    不仅消耗cpu,而且对我们的目的没什么帮助
redis中的近似随机LRU

实际上redis的LRU淘汰策略,放弃了严格的 淘汰最近最少使用的k/v。
默认随机采样5个k/v再根据其访问时间,淘汰掉 最近最少使用的 那个k/v。
循环直到释放出 需要的内存空间。

redis LRU的部分细节:

  1. 每个value都是一个redisObject,其中有一个字段 lru,记录了value的访问时间。
  2. 这个lru字段只有24位,精度是秒,194天后24位的lru字段会溢出,
    从0开始从新计算。
  3. redisObject的lru字段,是根据全局的 server.lruclock 字段赋值的,这也是个24位变量。若server.hz 等于 10,那系统的lru时钟每隔100ms会更新一次。

    取系统时间是系统调用,会切到内核执行,大量的取实时系统时钟也是很消耗系统资源的。
    redis出于对性能的优化,以秒为精度,缓存了系统的时钟。
    对系统时钟有特殊需求的,比如对key设置毫秒级过期时间时,会取实时的系统时钟。</

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值