智能摘要文章源自JAVA秀-https://www.javaxiu.com/30973.html
为每个键设置一个定时器,一旦过期时间到了,则将键删除。在Redis当中,其选择的是策略2和策略3的综合使用。volatile-lru根据LRU算法删除设置了过期时间的键,直到腾出可用空间。volatile-lfu根据LFU算法删除设置了过期时间的键,直到腾出可用空间。获取当前时间戳,转化为分钟后取低16位(为了方便后续计算,这个值记为now)。文章源自JAVA秀-https://www.javaxiu.com/30973.html
原文约 6605 字 | 图片 8 张 | 建议阅读 14 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/30973.html
天猫二面:内存耗尽后 Redis 会发生什么?
点击关注 ? Java基基 文章源自JAVA秀-https://www.javaxiu.com/30973.html
收录于话题文章源自JAVA秀-https://www.javaxiu.com/30973.html
#Java基基文章源自JAVA秀-https://www.javaxiu.com/30973.html
145个文章源自JAVA秀-https://www.javaxiu.com/30973.html
点击上方“Java基基”,选择“设为星标”文章源自JAVA秀-https://www.javaxiu.com/30973.html
做积极的人,而不是积极废人!文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html 源码精品专栏文章源自JAVA秀-https://www.javaxiu.com/30973.html
原创 | Java 2020 超神之路,很肝~文章源自JAVA秀-https://www.javaxiu.com/30973.html
中文详细注释的开源项目文章源自JAVA秀-https://www.javaxiu.com/30973.html
RPC 框架 Dubbo 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
网络应用框架 Netty 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
消息中间件 RocketMQ 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
数据库中间件 Sharding-JDBC 和 MyCAT 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
作业调度中间件 Elastic-Job 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
分布式事务中间件 TCC-Transaction 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
Eureka 和 Hystrix 源码解析文章源自JAVA秀-https://www.javaxiu.com/30973.html
Java 并发源码文章源自JAVA秀-https://www.javaxiu.com/30973.html
来源:cnblogs.com/lonely-wolf/p/14403264.html文章源自JAVA秀-https://www.javaxiu.com/30973.html
设置有效期文章源自JAVA秀-https://www.javaxiu.com/30973.html
过期策略文章源自JAVA秀-https://www.javaxiu.com/30973.html
8 种淘汰策略文章源自JAVA秀-https://www.javaxiu.com/30973.html
LRU 算法文章源自JAVA秀-https://www.javaxiu.com/30973.html
Redis 改进后的 LRU 算法文章源自JAVA秀-https://www.javaxiu.com/30973.html
Redis 如何管理热度数据文章源自JAVA秀-https://www.javaxiu.com/30973.html
LFU 算法文章源自JAVA秀-https://www.javaxiu.com/30973.html
访问频次递增文章源自JAVA秀-https://www.javaxiu.com/30973.html
访问频次递减文章源自JAVA秀-https://www.javaxiu.com/30973.html
总结
文章源自JAVA秀-https://www.javaxiu.com/30973.html
作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当 Redis
服务器的内存耗尽后,如果继续执行请求命令,Redis
会如何处理呢?文章源自JAVA秀-https://www.javaxiu.com/30973.html
设置有效期
使用Redis
服务时,很多情况下某些键值对只会在特定的时间内有效,为了防止这种类型的数据一直占有内存,我们可以给键值对设置有效期。Redis
中可以通过 4
个独立的命令来给一个键设置过期时间:文章源自JAVA秀-https://www.javaxiu.com/30973.html
expire key ttl
:将key
值的过期时间设置为ttl
秒 。文章源自JAVA秀-https://www.javaxiu.com/30973.htmlpexpire key ttl
:将key
值的过期时间设置为ttl
毫秒 。文章源自JAVA秀-https://www.javaxiu.com/30973.htmlexpireat key timestamp
:将key
值的过期时间设置为指定的timestamp
秒数 。文章源自JAVA秀-https://www.javaxiu.com/30973.htmlpexpireat key timestamp
:将key
值的过期时间设置为指定的timestamp
毫秒数 。文章源自JAVA秀-https://www.javaxiu.com/30973.html
PS:不管使用哪一个命令,最终 Redis
底层都是使用 pexpireat
命令来实现的。另外,set
等命令也可以设置 key
的同时加上过期时间,这样可以保证设值和设过期时间的原子性。文章源自JAVA秀-https://www.javaxiu.com/30973.html
设置了有效期后,可以通过 ttl
和 pttl
两个命令来查询剩余过期时间(如果未设置过期时间则下面两个命令返回 -1
,如果设置了一个非法的过期时间,则都返回 -2
):文章源自JAVA秀-https://www.javaxiu.com/30973.html
ttl key
返回key
剩余过期秒数。文章源自JAVA秀-https://www.javaxiu.com/30973.htmlpttl key
返回key
剩余过期的毫秒数。文章源自JAVA秀-https://www.javaxiu.com/30973.html
过期策略
如果将一个过期的键删除,我们一般都会有三种策略:文章源自JAVA秀-https://www.javaxiu.com/30973.html
定时删除 :为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对
CPU
不友好,因为每个定时器都会占用一定的CPU
资源。文章源自JAVA秀-https://www.javaxiu.com/30973.html惰性删除 :不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。文章源自JAVA秀-https://www.javaxiu.com/30973.html
定期扫描 :系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。文章源自JAVA秀-https://www.javaxiu.com/30973.html
在 Redis
当中,其选择的是策略 2
和策略 3
的综合使用。不过 Redis
的定期扫描只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis
会单独存储,所以不会出现扫描所有键的情况:文章源自JAVA秀-https://www.javaxiu.com/30973.html
typedef struct redisDb { dict *dict; //所有的键值对 dict *expires; //设置了过期时间的键值对 dict *blocking_keys; //被阻塞的key,如客户端执行BLPOP等阻塞指令时 dict *watched_keys; //WATCHED keys int id; //Database ID //... 省略了其他属性 } redisDb;
8 种淘汰策略
假如 Redis
当中所有的键都没有过期,而且此时内存满了,那么客户端继续执行 set
等命令时 Redis
会怎么处理呢?Redis
当中提供了不同的淘汰策略来处理这种场景。文章源自JAVA秀-https://www.javaxiu.com/30973.html
首先 Redis
提供了一个参数 maxmemory
来配置 Redis
最大使用内存:文章源自JAVA秀-https://www.javaxiu.com/30973.html
maxmemory <bytes>
或者也可以通过命令 config set maxmemory 1GB
来动态修改。文章源自JAVA秀-https://www.javaxiu.com/30973.html
如果没有设置该参数,那么在 32
位的操作系统中 Redis
最多使用 3GB
内存,而在 64
位的操作系统中则不作限制。文章源自JAVA秀-https://www.javaxiu.com/30973.html
Redis
中提供了 8
种淘汰策略,可以通过参数 maxmemory-policy
进行配置:文章源自JAVA秀-https://www.javaxiu.com/30973.html
淘汰策略 | 说明 |
---|---|
volatile-lru | 根据 LRU 算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-lru | 根据 LRU 算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-lfu | 根据 LFU 算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-lfu | 根据 LFU 算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-random | 随机删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-random | 随机删除所有键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-ttl | 根据键值对象的 ttl 属性, 删除最近将要过期数据。如果没有,则直接报错 |
noeviction | 默认策略,不作任何处理,直接报错 |
PS:淘汰策略也可以直接使用命令 config set maxmemory-policy <策略>
来进行动态配置。文章源自JAVA秀-https://www.javaxiu.com/30973.html
LRU 算法
LRU
全称为:Least Recently Used
。即:最近最长时间未被使用。这个主要针对的是使用时间。文章源自JAVA秀-https://www.javaxiu.com/30973.html
Redis 改进后的 LRU 算法
在 Redis
当中,并没有采用传统的 LRU
算法,因为传统的 LRU
算法存在 2
个问题:文章源自JAVA秀-https://www.javaxiu.com/30973.html
需要额外的空间进行存储。文章源自JAVA秀-https://www.javaxiu.com/30973.html
可能存在某些
key
值使用很频繁,但是最近没被使用,从而被LRU
算法删除。文章源自JAVA秀-https://www.javaxiu.com/30973.html
为了避免以上 2
个问题,Redis
当中对传统的 LRU
算法进行了改造,通过抽样的方式进行删除 。文章源自JAVA秀-https://www.javaxiu.com/30973.html
配置文件中提供了一个属性 maxmemory_samples 5
,默认值就是 5
,表示随机抽取 5
个 key
值,然后对这 5
个 key
值按照 LRU
算法进行删除,所以很明显,key
值越大,删除的准确度越高。文章源自JAVA秀-https://www.javaxiu.com/30973.html
对抽样 LRU
算法和传统的 LRU
算法,Redis
官网当中有一个对比图:文章源自JAVA秀-https://www.javaxiu.com/30973.html
浅灰色带是被删除的对象。文章源自JAVA秀-https://www.javaxiu.com/30973.html
灰色带是未被删除的对象。文章源自JAVA秀-https://www.javaxiu.com/30973.html
绿色是添加的对象。文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
左上角第一幅图代表的是传统 LRU
算法,可以看到,当抽样数达到 10
个(右上角),已经和传统的 LRU
算法非常接近了。文章源自JAVA秀-https://www.javaxiu.com/30973.html
Redis 如何管理热度数据
前面我们讲述字符串对象时,提到了 redisObject
对象中存在一个 lru
属性:文章源自JAVA秀-https://www.javaxiu.com/30973.html
typedef struct redisObject { unsigned type:4;//对象类型(4位=0.5字节) unsigned encoding:4;//编码(4位=0.5字节) unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节) int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节) void *ptr;//指向底层实际的数据存储结构,如:SDS等(8字节) } robj;
lru
属性是创建对象的时候写入,对象被访问到时也会进行更新。正常人的思路就是最后决定要不要删除某一个键肯定是用当前时间戳减去 lru
,差值最大的就优先被删除。但是 Redis
里面并不是这么做的,Redis
中维护了一个全局属性 lru_clock
,这个属性是通过一个全局函数 serverCron
每隔 100
毫秒执行一次来更新的,记录的是当前 unix
时间戳。文章源自JAVA秀-https://www.javaxiu.com/30973.html
最后决定删除的数据是通过 lru_clock
减去对象的 lru
属性而得出的。那么为什么 Redis
要这么做呢?直接取全局时间不是更准确吗?文章源自JAVA秀-https://www.javaxiu.com/30973.html
这是因为这么做可以避免每次更新对象的 lru
属性的时候可以直接取全局属性,而不需要去调用系统函数来获取系统时间,从而提升效率(Redis
当中有很多这种细节考虑来提升性能,可以说是对性能尽可能的优化到极致)。文章源自JAVA秀-https://www.javaxiu.com/30973.html
不过这里还有一个问题,我们看到,redisObject
对象中的 lru
属性只有 24
位,24
位只能存储 194
天的时间戳大小,一旦超过 194
天之后就会重新从 0
开始计算,所以这时候就可能会出现 redisObject
对象中的 lru
属性大于全局的 lru_clock
属性的情况。文章源自JAVA秀-https://www.javaxiu.com/30973.html
正因为如此,所以计算的时候也需要分为 2
种情况:文章源自JAVA秀-https://www.javaxiu.com/30973.html
当全局
lruclock
>lru
,则使用lruclock
-lru
得到空闲时间。文章源自JAVA秀-https://www.javaxiu.com/30973.html当全局
lruclock
<lru
,则使用lruclock_max
(即194
天) -lru
+lruclock
得到空闲时间。文章源自JAVA秀-https://www.javaxiu.com/30973.html
需要注意的是,这种计算方式并不能保证抽样的数据中一定能删除空闲时间最长的。这是因为首先超过 194
天还不被使用的情况很少,再次只有 lruclock
第 2
轮继续超过 lru
属性时,计算才会出问题。文章源自JAVA秀-https://www.javaxiu.com/30973.html
比如对象 A
记录的 lru
是 1
天,而 lruclock
第二轮都到 10
天了,这时候就会导致计算结果只有 10-1=9
天,实际上应该是 194+10-1=203
天。但是这种情况可以说又是更少发生,所以说这种处理方式是可能存在删除不准确的情况,但是本身这种算法就是一种近似的算法,所以并不会有太大影响。文章源自JAVA秀-https://www.javaxiu.com/30973.html
LFU 算法
LFU
全称为:Least Frequently Used
。即:最近最少频率使用,这个主要针对的是使用频率。这个属性也是记录在redisObject
中的 lru
属性内。文章源自JAVA秀-https://www.javaxiu.com/30973.html
当我们采用 LFU
回收策略时,lru
属性的高 16
位用来记录访问时间(last decrement time:ldt,单位为分钟),低 8
位用来记录访问频率(logistic counter:logc),简称 counter
。文章源自JAVA秀-https://www.javaxiu.com/30973.html
访问频次递增
LFU
计数器每个键只有 8
位,它能表示的最大值是 255
,所以 Redis
使用的是一种基于概率的对数器来实现 counter
的递增。r文章源自JAVA秀-https://www.javaxiu.com/30973.html
给定一个旧的访问频次,当一个键被访问时,counter
按以下方式递增:文章源自JAVA秀-https://www.javaxiu.com/30973.html
提取
0
和1
之间的随机数R
。文章源自JAVA秀-https://www.javaxiu.com/30973.htmlcounter
- 初始值(默认为5
),得到一个基础差值,如果这个差值小于0
,则直接取0
,为了方便计算,把这个差值记为baseval
。文章源自JAVA秀-https://www.javaxiu.com/30973.html概率
P
计算公式为:1/(baseval * lfu_log_factor + 1)
。文章源自JAVA秀-https://www.javaxiu.com/30973.html如果
R < P
时,频次进行递增(counter++
)。文章源自JAVA秀-https://www.javaxiu.com/30973.html
公式中的 lfu_log_factor
称之为对数因子,默认是 10
,可以通过参数来进行控制:文章源自JAVA秀-https://www.javaxiu.com/30973.html
lfu_log_factor 10
下图就是对数因子 lfu_log_factor
和频次 counter
增长的关系图:文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
可以看到,当对数因子 lfu_log_factor
为 100
时,大概是 10M(1000万)
次访问才会将访问 counter
增长到 255
,而默认的 10
也能支持到 1M(100万)
次访问 counter
才能达到 255
上限,这在大部分场景都是足够满足需求的。文章源自JAVA秀-https://www.javaxiu.com/30973.html
访问频次递减
如果访问频次 counter
只是一直在递增,那么迟早会全部都到 255
,也就是说 counter
一直递增不能完全反应一个 key
的热度的,所以当某一个 key
一段时间不被访问之后,counter
也需要对应减少。文章源自JAVA秀-https://www.javaxiu.com/30973.html
counter
的减少速度由参数 lfu-decay-time
进行控制,默认是 1
,单位是分钟。默认值 1
表示:N
分钟内没有访问,counter
就要减 N
。文章源自JAVA秀-https://www.javaxiu.com/30973.html
lfu-decay-time 1
具体算法如下:文章源自JAVA秀-https://www.javaxiu.com/30973.html
获取当前时间戳,转化为分钟 后取低
16
位(为了方便后续计算,这个值记为now
)。文章源自JAVA秀-https://www.javaxiu.com/30973.html取出对象内的
lru
属性中的高16
位(为了方便后续计算,这个值记为ldt
)。文章源自JAVA秀-https://www.javaxiu.com/30973.html当
lru
>now
时,默认为过了一个周期(16
位,最大65535
),则取差值65535-ldt+now
:当lru
<=now
时,取差值now-ldt
(为了方便后续计算,这个差值记为idle_time
)。文章源自JAVA秀-https://www.javaxiu.com/30973.html取出配置文件中的
lfu_decay_time
值,然后计算:idle_time / lfu_decay_time
(为了方便后续计算,这个值记为num_periods
)。文章源自JAVA秀-https://www.javaxiu.com/30973.html最后将
counter
减少:counter - num_periods
。文章源自JAVA秀-https://www.javaxiu.com/30973.html
看起来这么复杂,其实计算公式就是一句话:取出当前的时间戳和对象中的 lru
属性进行对比,计算出当前多久没有被访问到,比如计算得到的结果是 100
分钟没有被访问,然后再去除配置参数 lfu_decay_time
,如果这个配置默认为 1
也即是 100/1=100
,代表 100
分钟没访问,所以 counter
就减少 100
。文章源自JAVA秀-https://www.javaxiu.com/30973.html
总结
本文主要介绍了 Redis
过期键的处理策略,以及当服务器内存不够时 Redis
的 8
种淘汰策略,最后介绍了 Redis
中的两种主要的淘汰算法 LRU
和 LFU
。文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
已在知识星球更新源码解析如下:文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。文章源自JAVA秀-https://www.javaxiu.com/30973.html
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。文章源自JAVA秀-https://www.javaxiu.com/30973.html
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章源自JAVA秀-https://www.javaxiu.com/30973.html
文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)文章源自JAVA秀-https://www.javaxiu.com/30973.html
阅读原文文章源自JAVA秀-https://www.javaxiu.com/30973.html

评论