Redis的淘汰策略详解

接上一篇Redis的过期策略详解

Redis的过期策略详解

所谓的淘汰策略就是:
我们redis中的数据都没有过期,但是内存有大小,所以我们得淘汰一些没有过期的数据!!

那么怎么去淘汰了,我们上一篇讲了冰箱其实也是相当于一个缓存容器,放菜!!

那么如果现在冰箱里面的菜都是好的没过期的,但是你家冰箱满了,买新冰箱又来不及,要去扔菜或者把它吃掉!就是要清理菜!你们会怎么清理?
官网:Redis淘汰策略官网地址
官网目前给出了8种策略(4.0版本以后加入了LFU),但是咱们这里只重点研究LRU和LFU,其他的像随机这种,一看就懂,我们自行看一下就好

PS:是在config文件中配置的策略:
#maxmemory-policy noeviction
默认就是不淘汰,如果满了,能读不能写!就跟你冰箱满了一样,能吃,不能放!

LRU

Least Recently Used
翻译过来是最久未使用,根据时间轴来走,很久没用的数据只要最近有用过,我就默认是有效的。

也就是说这个策略的意思是先淘汰长时间没用过的

那么怎么去判断一个redis数据有多久没访问了,Redis是这样做的

redis的所有数据结构的对外对象里,它里面有个字段叫做lru

源码:server.h 620行

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;

每个对象都有1个LRU的字段,看它的说明,好像LRU跟我们后面讲的LFU都跟这个字段有关,并且这个lru代表的是一个时间相关的内容。
那么我们大概能猜出来,redis去实现lru肯定跟我们这个对象的lru相关!!
首先,我告诉大家,这个lru在LRU算法的时候代表的是这个数据的访问时间的秒单位!!但是只有24bit,无符号位,所以这个lru记录的是它访问时候的秒单位时间的后24bit!
用Java来写就是:(不会有人不知道redis是C写的吧#手动滑稽)

long timeMillis=System.currentTimeMillis();
System.out.println(timeMillis/1000); //获取当前秒
System.out.println(timeMillis/1000 & ((1<<24)-1));//获取秒的后24位

我们刚才讲了,是获取当前时间的最后24位,那么当我们最后的24bit都是1了的时候,时间继续往前走,那么我们获取到的时间是不是就是24个0了!
举个例子:

11111111111111111000000000011111110 假如这个是我当前秒单位的时间,获取后8位 是 11111110
11111111111111111000000000011111111 获取后8位 是11111111
11111111111111111000000000100000000 获取后8位 是00000000

所以,它有个轮询的概念,它如果超过24位,又会从0开始!所以我们不能直接的用系统时间秒单位的24bit位去减对象的lru,而是要判断一下,还有一点,为了性能,我们系统的时间不是实时获取的,而是用redis的时间事件来获取,所以,我们这个时间获取是100ms去获取一次

如图:

在这里插入图片描述


好,现在我们知道了原来redis对象里面原来有个字段是记录它访问时间的,那么接下来肯定有个东西去跟这个时间去比较,拿到差值!
我们去看下源码evict.c文件

unsigned long long estimateObjectIdleTime(robj *o) {
	//获取秒单位时间的最后24位
	unsigned long long lruclock = LRU_CLOCK();
	//因为只有24位,所有最大的值为2的24次方-1
	//超过最大值从0开始,所以需要判断lruclock(当前系统时间)跟缓存对象的lru字段的大小
	if (lruclock >= o->lru) {
	//如果lruclock>=robj.lru,返回lruclock-o->lru,再转换单位
	return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
	} else {
	//否则采用lruclock + (LRU_CLOCK_MAX - o->lru),得到对象的值越小,返回的值越大,越大越容易被淘汰
	return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
	LRU_CLOCK_RESOLUTION;
	}
}

我们发现,跟对象的lru比较的时间也是serverCron下面的当前时间的秒单位的后面24位!但是它有个判断,有种情况是系统时间跟对象的LRU的大小,因为最大只有24位,会超过!!

所以,我们可以总结下我们的结论

  1. Redis数据对象的LRU用的是server.lruclock这个值,server.lruclock又是每隔100ms生成的系统时间的秒单位的后24位!所以server.lruclock可以理解为延迟了100ms的当前时间秒单位的后24位!
  2. 用server.lruclock 与 对象的lru字段进行比较,因为server.lruclock只获取了当前秒单位时间的后24位,所以肯定有个轮询。所以,我们会判断server.lruclock跟对象的lru字段进行比较,如果server.lruclock>obj.lru,那么我们用server.lruclock-obj.lru,否则server.lruclock+(LRU_CLOCK_MAX-obj.lru),得到lru越小,那么返回的数据越大,相差越大的就会被淘汰!

LFU

Least Frequently Used 翻译成中文就是最不常用,不常用的衡量标准就是使用次数

但是LFU的有个致命的缺点!就是它只会加不会减!为什么说这是个缺点

举个例子:去年有一个热搜,今年有一个热搜,热度相当,但是去年的那个因为有时间的积累,所以点击次数高,今年的点击次数因为刚发生,所以累积起来的次数没有去年的高,那么我们如果已点击次数作为衡量,是不是应该删除的就是今年的?这就导致了新的进不来旧的出不去

所以我们来看redis它是怎么实现的!怎么解决这个缺点!

我们还是来看我们的redisObject(server.h 620行)

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;

我们看它的lru,它如果被用作LFU的时候!它前面16位代表的是时间,后8位代表的是一个数值,frequency是频率,应该就是代表这个对象的访问次数,我们先给它叫做counter。
那么这个16位的时间跟8位的counter到底有啥用呢?8位我们还能猜测下,可能就是这个对象的访问次数!
我们淘汰的时候,是不是就是去根据这个对象使用的次数,按照小的就去给它淘汰掉。
其实,差不多就是这么个意思。
还有个问题,如果8位用作访问次数的话,那么8位最大也就2的8次方,就是255次,够么?如果,按照我们的想法,肯定不够,那么redis去怎么解决呢?
既然不够,那么让它不用每次都加就可以了,能不能让它值越大,我们加的越慢就能解决这个问题
redis还加了个东西,让你们自己能主宰它加的速率,这个东西就是lfu-log-factor!它配置的越大,那么对象的访问次数就会加的越慢。
源码:
(evict.c 328行)

uint8_t LFULogIncr(uint8_t counter) {
	//如果已经到最大值255,返回255 ,8位的最大值
	if (counter == 255) return 255;
	//得到随机数(0-1)
	double r = (double)rand()/RAND_MAX;
	//LFU_INIT_VAL表示基数值(在server.h配置)
	double baseval = counter - LFU_INIT_VAL;
	//如果达不到基数值,表示快不行了,baseval =0
	if (baseval < 0) baseval = 0;
	//如果快不行了,肯定给他加counter
	//不然,按照几率是否加counter,同时跟baseval与lfu_log_factor相关
	//都是在分子,所以2个值越大,加counter几率越小
	double p = 1.0/(baseval*server.lfu_log_factor+1);
	if (r < p) counter++;
	return counter;
}

所以,LFU加的逻辑我们可以总结下:

  1. 如果已经是最大值,我们不再加!因为到达255的几率不是很高!可以支撑很大很大的数据量!
  2. counter属于随机添加,添加的几率根据已有的counter值和配置lfu-log-factor相关,counter值越大,添加的几率越小,lfu-log-factor配置的值越大,添加的几率越小!

还有,这个前16bit时间到底是干嘛的!!我们现在还不知道,传统的LFU只能加,不会减。
那么我们想下,这个时间是不是就是用来减次数的?
大家有玩王者充钱的么,如果充钱的同学应该知道,如果你很久很久不充钱的话,你的vip等级会降,诶,这个是不是就能解决我们的次数只能加不能减的问题!并且这个减还是根据你多久时间没充钱来决定的,所以,我们可以大胆猜下,这个前16bit的时间是不是也记录了这个对象的时间,然后根据这个时间判断这个对象多久没访问了就去减次数了。

没错,你猜的都是对的!我们的这个16bit记录的是这个对象的访问时间的分单位的后16位,每次访问或者操作的时候,都会去跟当前时间的分单位的后16位去比较得到多少分钟没有访问了!然后去减去对应的次数
那么这个次数每分钟没访问减多少了,就是根据我们的配置lfu-decay-time。
这样就能够实现我们的LFU,并且还解决了LFU不能减的问题。
总结如图:

在这里插入图片描述


贴出减的源码:

unsigned long LFUDecrAndReturn(robj *o) {
	//lru字段右移8位,得到前面16位的时间
	unsigned long ldt = o->lru >> 8;
	//lru字段与255进行&运算(255代表8位的最大值),
	//得到8位counter值
	unsigned long counter = o->lru & 255;
	//如果配置了lfu_decay_time,用LFUTimeElapsed(ldt) 除以配置的值
	//LFUTimeElapsed(ldt)源码见下
	//总的没访问的分钟时间/配置值,得到每分钟没访问衰减多少
	unsigned long num_periods = server.lfu_decay_time ?LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
	if (num_periods)
	//不能减少为负数,非负数用couter值减去衰减值
	counter = (num_periods > counter) ? 0 : counter - num_periods;
	return counter;
}

LFUTimeElapsed方法源码(evict.c):

//对象ldt时间的值越小,则说明时间过得越久
unsigned long LFUTimeElapsed(unsigned long ldt) {
	//得到当前时间分钟数的后面16位
	unsigned long now = LFUGetTimeInMinutes();
	//如果当前时间分钟数的后面16位大于缓存对象的16位
	//得到2个的差值
	if (now >= ldt) return now-ldt;
	//如果缓存对象的时间值大于当前时间后16位值,则用65535-ldt+now得到差值
	return 65535-ldt+now;
}

所以,LFU减逻辑我们可以总结下:

  1. 我们可以根据对象的LRU字段的前16位得到对象的访问时间(分钟),
    根据跟系统时间比较获取到多久没有访问过!
  2. 根据lfu-decay-time(配置),代表每分钟没访问减少多少counter,不
    能减成负数

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读1.3k次。在 Redis 中,键(Keys)是非常重要的概念,它们代表了存储在数据库中的数据的标识符。对键的有效管理和操作是使用 Redis 数据库的关键一环,它直接影响到数据的存取效率、系统的稳定性和开发的便利性。本文将深入探讨 Redis 中键的管理和操作,包括键的命名规范、常用的键操作命令以及一些最佳实践。我们将详细介绍如何合理命名键、如何使用键的过期和持久化特性、如何批量删除键等技巧,旨在帮助读者更好地理解并灵活运用 Redis 中的键,从而提高数据管理和操作的效率和可靠性。
文章浏览阅读3.3k次,点赞44次,收藏88次。本篇是对单节点的应用,但从中我们也能推断出一些关于集群的应用,不过大多数公司能搞个主从就已经是不错了,所以你能学会这个已经算是很有用了,关于ES,博主前面也讲过一些基础应用,创建一个工具类利用ES的数据模型进行存储就可以达到一个canal同时对Redis和ES的同步,如果担心出问题,可以把Canal搞成集群的形式,这个后续有时间博主再给大家做讲解。今天就到这里了,觉得不错就支持一下吧。_canal redis
文章浏览阅读8.4k次,点赞8次,收藏18次。Spring Boot 整合Redis实现消息队列,RedisMessageListenerContainer的使用,Pub/Sub模式的优缺点_springboot redis 消息队列
文章浏览阅读978次,点赞25次,收藏21次。在Centos上安装Redis5.0保姆级教程!_centos7 安装redis5.0服务器
文章浏览阅读1.2k次,点赞21次,收藏22次。Docker-Compose部署Redis(v7.2)主从模式首先需要有一个redis主从集群,才能接着做redis哨兵模式。_warning: sentinel was not able to save the new configuration on disk!!!: dev
文章浏览阅读2.2k次,点赞59次,收藏38次。合理的JedisPool资源池参数设置能为业务使用Redis保驾护航,本文将对JedisPool的使用、资源池的参数进行详细说明,最后给出“最合理”配置。_jedispool资源池优化
文章浏览阅读1.9k次。批量删除指定前缀的Key有两中方法,一种是借助 redis-cli,另一种是通过 SCAN命令来遍历所有匹配前缀的 key,并使用 DEL命令逐个删除它们。_redis删除前缀的key
文章浏览阅读890次,点赞18次,收藏20次。1. Redis时一个key-cakye的数据库,key一般是String类型,不过value类型有很多。eg.String Hash List Set SortedSet (基本) | GEO BitMap HyperLog (特殊)2.Redis为了方便学习,将操作不同类型的命令做了分组,在官网可以进行查询。
文章浏览阅读1.1k次,点赞19次,收藏26次。若不使用Redisson,而是用synchronized(this),此时会造成对服务器的加锁,若开始大量查询ID为1的商品,每台机器都会先跑一遍加个锁,然后在查询ID为2的数据,此时需要等待ID为1的锁释放,所以需要将this对象调整为全局商品ID。若在执行bgsave命令时,还有其他redis命令被执行(主线程数据修改),此时会对数据做个副本,然后bgsave命令执行这个副本数据写入rdb文件,此时主线程还可以继续修改数据。在当前redis目录下会生成aof文件,对redis修改数据的命令进行备份。
文章浏览阅读1.5k次,点赞39次,收藏24次。本文全面剖析Redis集群在分布式环境下的数据一致性问题,从基础原理到高级特性,涵盖主从复制、哨兵模式、持久化策略等关键点,同时也分享了关于监控、故障模拟与自适应写一致性策略的实践经验。_redis集群一致性
文章浏览阅读1k次。RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令。AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF。
文章浏览阅读1k次,点赞24次,收藏21次。NoSQL(No only SQL)数据库,泛指非关系型数据库,实现对于传统数据库而言的。NoSQL 不依赖业务逻辑方式进行存储,而以简单的 key-value 模式存储。因此大大增加了数据库的扩展能力。不遵循SQL标准不支持ACID远超于SQL的性能Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储。
文章浏览阅读988次,点赞17次,收藏19次。在上面的步骤中,我们已经开启了 MySQL 的远程访问功能,但是,如果使用 MySQL 管理工具 navicat 连接 MySQL 服务端时,还是可能会出现连接失败的情况。在实际工作中,如果我们需要从其他地方访问和管理 MySQL 数据库,就需要开启 MySQL 的远程访问功能并设置相应的权限。这对于我们的工作效率和数据安全都有很大的帮助。通过查看 MySQL 用户表,我们可以看到’host’为’%’,说明 root 用户登录 MySQL 的时候,可以允许任意的 IP 地址访问 MySQL 服务端。
文章浏览阅读956次。Redis Desktop Manager(RDM)是一款用于管理和操作Redis数据库的图形化界面工具。提供了简单易用的界面,使用户能够方便地执行各种Redis数据库操作,并且支持多个Redis服务器的连接_redisdesktopmanager安装包
文章浏览阅读1.9k次,点赞52次,收藏27次。缓存击穿指的是数据库有数据,缓存本应该也有数据,但是缓存过期了,Redis 这层流量防护屏障被击穿了,请求直奔数据库。缓存穿透指的是数据库本就没有这个数据,请求直奔数据库,缓存系统形同虚设。缓存雪崩指的是大量的热点数据无法在 Redis 缓存中处理(大面积热点数据缓存失效、Redis 宕机),流量全部打到数据库,导致数据库极大压力。
文章浏览阅读1.2k次。一次命令时间(borrow|return resource + Jedis执行命令(含网络) )的平均耗时约为1ms,一个连接的QPS大约是1000,业务期望的QPS是50000,那么理论上需要的资源池大小是50000 / 1000 = 50个,实际maxTotal可以根据理论值合理进行微调。JedisPool默认的maxTotal=8,下面的代码从JedisPool中借了8次Jedis,但是没有归还,当第9次(jedisPool.getResource().ping())3、发生异常可能的情况。_redis.clients.jedis.exceptions.jedisconnectionexception: could not get a res
文章浏览阅读1k次,点赞27次,收藏18次。在这篇文章中,你将了解到如何在 CentOS 系统上安装 Redis 服务,并且掌握通过自定义域名来访问 Redis 服务的技巧。通过使用自定义域名,你可以方便地管理和访问你的 Redis 数据库,提高工作效率。无论你是开发者、系统管理员还是对 Redis 感兴趣的读者,这篇文章都会为你提供清晰的指导和实用的技巧。阅读本文,轻松搭建自己的 Redis 服务,并体验自定义域名带来的便捷!_redis怎么自定义域名
文章浏览阅读1.1k次,点赞15次,收藏18次。我们post请求,拦截器要预先读取HtppServletRequest里面的body的数据,是通过io的方式,都知道io读取完毕之后,之前的数据是变为null的,但是,当我么后面的接口来委派的时候,也是通过io读取body。我们要考虑一个事情,就是我们要验证数据的重复提交: 首先第一次提交的数据肯定是要被存储的,当而第二次往后,每次提交数据都会与之前的数据产生比对从而验证数据重复提交,我们要具体判断数据是否重复提交的子类。发现数据是成功存入的,剩余7s过期,在10s之内,也就是数据没过期之前,在发送一次。_json.parseobject(str, clazz, auto_type_filter);
文章浏览阅读3.9k次,点赞3次,收藏7次。PHP使用Redis实战实录系列:我们首先检查$redis->connect()方法的返回值来确定是否成功连接到Redis服务器。如果连接失败,我们可以输出相应的错误信息。如果连接成功,我们再执行一些操作,如$redis->set()、$redis->get()等,并检查每个操作的返回结果来判断是否发生了异常。_php redis
文章浏览阅读1.5w次,点赞23次,收藏51次。Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。Redis 是一个高性能的key-value数据库。redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。_redisdesktopmanager下载