redis——Redis中的LRU算法改进

redis通常使用缓存,是使用一种固定最大内存的使用。当数据达到可使用的最大固定内存时,我们需要通过移除老数据来获取空间。redis作为缓存是否有效的重要标志是如何寻找一种好的策略:删除即将需要使用的数据是一种糟糕的策略,而删除那些很少再次请求的数据则是一种好的策略。
在其他的缓存组件还有个命中率,仅仅表示读请求的比例。访问一个缓存中的keys通常不是分布式的。然而访问经常变化,这意味着不经常访问,相反,有些keys一旦不流行可能会转向最经常访问的keys。 因此,通常一个缓存系统应该尽可能保留那些未来最有可能被访问的keys。针对keys淘汰的策略是:那些未来极少可能被访问的数据应该被移除。
但有一个问题:redis和其他缓存系统不能够预测未来。

LRU算法

缓存系统不能预测未来,原因是:那些很少再次被访问的key也很有可能最近访问相当频繁。如果经常被访问的模式不会突然改变,那么这是一种很有效的策略。然而,“最近经常被访问”似乎更隐晦地标明一种 理念。这种算法被称为LRU算法。最近访问频繁的key相比访问少的key有更高的可能性。
举个例子,这里有4个不同访问周期的key,每一个“~”字符代表一秒,结尾的“|”表示当前时刻。

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

A key每5秒请求一次,B周期是2秒,C、D都是10秒。
访问频率最高的是B,因为它的空闲时间最短,这意味着B是4个key中未来最有可能被访问的key。
同样的A和C目前的空闲时间是2s和6s也能很好地反映它们本身的周期。然而你可以看到不够严谨:D的访问周期是10秒,但它却是4个key中最近被访问的。
当然,在一个很长的运行周期中,LRU算法能工作得很好。通常有一个更高访问频率的key当然有一个更低的空闲周期。LRU算法淘汰最少被访问key,那些有最大空闲周期的key。实现上也相当容易,只需要额外跟踪最近被访问的key即可,有时甚至都需要:把所有我们想要淘汰的对象放到一个链表中,当一个对象访问就移除链表头部元素,当我们要淘汰元素是就直接淘汰链表尾部开始。

redis中的LRU:起因

最初,redis不支持LRU算法。当内存有效性成为一个必须被解决的问题时,后来才加上了。通过修改redis对象结构,在每个key对象增加24bit的空间。没有额外的空间使用链表把所有对象放到一个链表中(大指针),因此需要实现得更加有效,不能因为key淘汰算法而让整个服务改动太大。
24bits的对象已经足够去存储当前的unxi时间戳。这个表现,被称为“LRU 时钟”,key元数据经常被更新,所以它是一个有效的算法。
然后,有另一个更加复杂的问题需要解决:如何选择访问间隔最长的key,然后淘汰它。
redis内部采用一个庞大的hash table来保存,添加另外一个数据结构存储时间间隔显然不是一个好的选择。然而我们希望能达到一个LRU本身是一个近似的,通过LRU算法本身来实现。

redis原始的淘汰算法简单实现:**当需要淘汰一个key时,随机选择3个key,淘汰其中间隔时间最长的key。**基本上,我们随机选择key,淘汰key效果很好。后来随机3个key改成一个配置项"N随机key"。但把默认值提高改成5个后效果大大提高。考虑到它的效果,你根本不用修改他。

然而,你可能会想这个算法如何有效执行,你可以看到我们如何捣毁了很多有趣的数据。也许简单的N key,我们会遇到很多好的决策,但是当我们淘汰最好的,下一个周期又开始抓。

验证规则第一条:用肉眼观察你的算法

其中有一个观点已经应用到Redis 3.0正式版中了。在redis2.8中一个LRU缓存经常被使用在多个环境,用户关于淘汰的没有抱怨太多,但是很明显我可以提高它,通过不仅仅是增加额外的空间,还有额外的CPU时间。
然而为了提高某项功能,你必须观察它。有多个不同的方式去观察LRU算法。你可以通过写工具观察,例如模拟不同的工作负载、校验命中率和失误率。
程序非常简单:增加一些指定的keys,然后频繁地访问这些keys以至于每一个key都有一个下降的空闲时间。最终超过50%的keys被增加,一半的老key需要被淘汰。
一个完美理想的LRU实现,应该是没有最新加的key被淘汰,而是淘汰最初的50%的老key。

规则二:不要丢弃重要信息

借助最新的可视化工具,我可以在尝试新的方法观察和测试几分钟。使用redis最明显有效的提高算法就是,积累对立的垃圾信息在一个淘汰池中。
基本上,当N keys算法被采用时,通常会分配一个很大的线程pool(默认为16key),这个池按照空闲时间排序,所以只有当有一个大于池中的一个或者池为空的时候,最新的key只会进入到这个池中。
同时,一个新的redis-cli模式去测量LRU算法也增加了(看这个-lru-test选项)。
还有另外一个方式去检验LRU算法的好坏,通过一个幂等访问模式。这个工具通常校验用一个不同的测试,新算法工作工作效果好于真实世界负载。它也同样使用流水线和每秒打印访问日志,因此可以被使用不用为了基准不同的思想,至少可以校验和观察明显的速度回归。

规则三、最少使用原则(LFU算法)


一切源于一个开放性问题:但你有多个redis 3.2数据库时,而淘汰算法只能在本机选择。因此,假如你全部空闲小的key都是DB0号机器,空闲时间长的key都是1号机器,redis每台机器都会淘汰各自的key。一个更好的选择当然是先淘汰DB1,最后再淘汰DB0。
当redis被当作缓存使用时很少有情况被分成不同的db上,这不是一个好的处理方式。然而这也是我为什么我再一次修改淘汰代码的原因。最终,我能够修改缓存池包括数据库id,使用单缓存池为多个db,代替多缓存池。这种实现很麻烦,但是通过优化和修改代码,最终它比普通实现要快到20%。
然而这时候,我对这个redis缓存淘汰算法的好奇心又被点燃。我想要提升它。我花费了几天想要提高LRU算法实现:或许可以使用更大的缓存池?通过历史时间选择最合适被淘汰的key?
经过一段时间,通过优化我的工具,我理解到:LRU算法受限于数据库中的数据样本,有时可能相反的场景效果非常好,因此要想提高非常非常难。实际上,能通过展示不同算法的图片上看这有点非常明显:每个周期10个keys几乎和理论的LRU算法表现一致。
当原始算法很难提高时,我开始测试新的算法。 如果我们倒回到博客开始,我们说过LRU实际上有点严格。哪些key需要我们真正想要保留:将来有最大可能被访问,最频繁被访问,而不是最近被访问的key。
淘汰最少被访问的key算法成为:LFU(Least Frequently Used),将来要被淘汰腾出新空间给新key。
理论上LFU的思想相当简单,只需要给每个key加一个访问计数器。每次访问就自增1,所以也就很容易知道哪些key被访问更频繁。
当然,LFU也会带起其他问题,不单单是针对redis,对于LFU实现:
1、不能使用“移除顶部元素”的方式,keys必须要根据访问计数器进行排序。每访问一次就得遍历所有key找出访问次数最少的key。
2、LFU不能仅仅是只增加每一访问的计数器。正如我们所讲的,访问模式改变随时变化,因此一个有高访问次数的key,后面很可能没有人继续访问它,因此我们的算法必须要适应超时的情况。
在redis中,第一个问题很好解决:我们可以在LRU的方式一样:随机在缓存池中选举,淘汰其中某项。第二个问题redis还是存在,因此一般对于LFU的思想必须使用一些方式进行减少,或者定期把访问计数器减半。

24位的LFU实现

LFU有它本身的实现,在redis中我们使用自己的24bit来记录LRU。
为了实现LFU仅仅需要在每个对象额外新增24bit:
1、一部分用于保存访问计数器;
2、足够用于决定什么时候将计数器减半的信息;

我的解决方法是把24bit分成两列:

16bits8bitslast decr timeLOG_C

16位记录最后一次减半时间,那样redis知道上一次减半时间,另外8bit作为访问计数器。
你可能会想8位的计数器很快就会溢出,是的,相对于简单计数器,我采用逻辑计数器。逻辑计数器的实现:

uint8_t LFULogIncr(uint8_t counter) {if (counter == 255) return 255;double r = (double)rand()/RAND_MAX;double baseval = counter - LFU_INIT_VAL;if (baseval < 0) baseval = 0;double p = 1.0/(baseval*server.lfu_log_factor+1);if (r < p) counter++;return counter;}

基本上计数器的较大者,更小的可能计数器会增加:上面的代码计算p位于0~1之间,但计数器增长时会越来越小,位于0-1的随机数r,只会但满足r<p时计数器才会加一。
你可以配置计数器增长的速率,如果使用默认配置,会发生:

  • 100次访问后,计数器=10;
  • 1000次访问是是18;
  • 10万次访问是142;
  • 100万次访问后达到255,并不在继续增长;

下面,让我们看看计数器如果进行衰减。16位的被储存为unix时间戳保留到分钟级别,redis会随机扫描key填充到缓存池中,如果最后一个下降的时间大于N分钟前(可配置化),如果计数器的值很大就减半,或者对于值小的就直接简单减半。
这里又衍生出另外一个问题,就是新进来的key是需要有机会被保留的。由于LFU新增是得分都是0,非常容易被选举替换掉。在redis中,开始默认值为5。这个初始值是根据增长数据和减半算法来估算的。模拟显示得分小于5的key是首选。

代码和性能

上面描述的算法已经提交到一个非稳定版的redis分支上。我最初的测试显示:它在幂等模式下优于LRU算法,测试情况是每个key使用用相同数量的内存,然而真实世界的访问可能会有很大不同。时间和空间都可能改变得很不同,所以我会很开心去学习观察现实世界中LFU的性能如何,两种方式在redis实现中对性能的改变。
因此,新增了一个OBJECT FREQ子命令,用于报告给定key的访问计数器,不仅仅能有效提观察一个计数器,而且还能调试LFU实现中的bug。
注意运行中切换LRU和LFU,刚开始会随机淘汰一些key,随着24bit不能匹配上,然而慢慢会适应。 还有几种改进实现的可能。Ben Manes发给我这篇感兴趣的文章,描述了一种叫TinyLRU算法。链接

这篇文章包含一个非常厉害的观点:相比于记录当前对象的访问频率,让我们(概率性地)记录全部对象的访问频率,看到了,这种方式我们甚至可以拒绝新key,同样,我们相信这些key很可能得到很少的访问,所以一点也不需要淘汰,如果淘汰一个key意味着降低命中/未命中率。
我的感觉这种技术虽然很感兴趣GET/SET LFU缓存,但不适用与redis性质的数据服务器:用户期望keys被创建后至少存在几毫秒。拒绝key的创建似乎在redis上就是一种错误。
然而,redis保留了LFU信息,当一个key被覆盖时,举个例子:

SET oldkey some_new_value

24位的LFU计数器会从老的key复制到新对象中。

新的redis淘汰算法不稳定版本还有以下几个好消息:
1、跨DB策略。在过去的redis只是基于本地的选举,现在修复为所有策略,不仅仅是LRU。
2、易变ttl策略。基于key预期淘汰存活时间,如今就像其他策略中的使用缓存池。
3、在缓存池中重用了sds对象,性能更好。

这篇博客比我预期要长,但是我希望它反映出一个见解:在创新和对于已经存在的事物实现上,一种解决方案去解决一个特定问题,一个基础工具。由开发人员以正确的方式使用它。许多redis的用户把redis作为一个缓存的解决方案,因此提高淘汰策略这一块经常一次又一次被拿出来探讨。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/445275.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis——HyperLogLog

HyperLogLog 是一种概率数据结构&#xff0c;用来估算数据的基数。数据集可以是网站访客的 IP 地址&#xff0c;E-mail 邮箱或者用户 ID。 基数就是指一个集合中不同值的数目&#xff0c;比如 a, b, c, d 的基数就是 4&#xff0c;a, b, c, d, a 的基数还是 4。虽然 a 出现两次…

机器学习知识总结系列-机器学习中的优化算法总结(1-4)

文章目录1.梯度下降1.1批量梯度下降(BGD)1.2随机梯度下降&#xff08;SGD&#xff09;1.3 小批量随机梯度下降&#xff08;MSGD&#xff09;1.4 比较&#xff1a;1.5 动量算法&#xff08;momentum&#xff09;1.6 Nestrov Momentum2. 自适应方法2.1 自适应学习率算法&#xff…

Python(19)-字符串、Unicode字符串

高级数据类型--字符串、Unicode字符串1.字符串的定义2.字符串的长度、计数、Index3.字符串常用方法3.1判断类型3.2查找和替换3.3文本对齐3.4去除空白字符.strip()4.字符串的拆分和拼接5.字符串的切片6.跨行字符串7.包含转义字符r8.字符串的分割与连接9.Unicode字符串字符串-不变…

机器学习中的距离和损失函数

文章目录13.1 距离度量13.2 损失函数13.1 距离度量 距离函数种类&#xff1a;欧式距离、曼哈顿距离、明式距离&#xff08;闵可夫斯基距离&#xff09;、马氏距离、切比雪夫距离、标准化欧式距离、汉明距离、夹角余弦等常用距离函数&#xff1a;欧式距离、马氏距离、曼哈顿距离…

Python(20)-高级数据类型的公共方法

高级数据类型的公共方法1内置函数2高级数据类型切片3运算符&#xff0c;*&#xff0c;in4完整的for循环公共方法是列表&#xff0c;元组&#xff0c;字典&#xff0c;字符串都能使用的方法1内置函数 内置函数&#xff1a;不需要import导入模块&#xff0c;就可以直接使用的函数…

redis——为什么选择了跳表而不是红黑树?

跳表是个啥东西请看这个文章。 我们知道&#xff0c;节点插入时随机出一个层数&#xff0c;仅仅依靠一个简单的随机数操作而构建出来的多层链表结构&#xff0c;能保证它有一个良好的查找性能吗&#xff1f;为了回答这个疑问&#xff0c;我们需要分析skiplist的统计性能。 在…

机器学习公式推导

文章目录线性回归逻辑回归线性判别分析PCAk-means决策树svm随机深林GBDTxgboost强化学习MapReduce线性回归 逻辑回归 对于分类问题&#xff1a;输出0/1&#xff0c;超过[0,1]没有意义&#xff0c;使用sigmoid函数 **代价函数&#xff1a;**使用L2平方差&#xff0c;由于模型函…

Python综合应用(1)--名片管理系统开发

第一个综合应用-名片管理系统1框架搭建2完善功能综合应用&#xff0c;名片管理系统 欢迎界面&#xff0c;不同选项&#xff0c;1.新建名片&#xff0c;2.显示全部&#xff0c;3 查询名片&#xff08;查到之后可以修改名片信息&#xff09;&#xff0c;0 退出系统 程序开发流程…

springboot1——spring相关入门

spring 随着我们开发&#xff0c;发现了一个问题&#xff1a; A---->B---->C---->D 在A中创建B的对象调用B的资源 在B中创建C的对象调用C的资源 在C中创建D的对象调用…

大数据学习(06)-- 云数据库

文章目录目录1.什么是云数据库&#xff1f;1.1 云计算和云数据库的关系1.2 云数据库的概念1.3 云数据库的特性1.4 云数据库应用场景1.5 云数据库和其他数据的关系2.云数据库产品有哪些&#xff1f;2.1 云数据库厂商概述2.2 亚马逊云数据库产品2.3 Google云数据库产品2.4 微软云…

Python(21)--变量进阶

变量的进阶使用1变量引用2可变、不可变数据类型3局部变量和全局变量4.Tips本系列博文来自学习《Python基础视频教程》笔记整理&#xff0c;视屏教程连接地址&#xff1a;http://yun.itheima.com/course/273.html在博文&#xff1a;https://blog.csdn.net/sinat_40624829/articl…

HTTP 响应代码全集

HTTP 响应状态代码指示特定 http 请求是否已成功完成。响应分为五类&#xff1a;信息响应(100–199)&#xff0c;成功响应(200–299)&#xff0c;重定向(300–399)&#xff0c;客户端错误(400–499)和服务器错误 (500–599)。状态代码由 section 10 of RFC 2616定义 信息响应 …

机器学习知识总结系列-机器学习中的数学-矩阵(1-3-2)

矩阵 SVD 矩阵的乘法状态转移矩阵状态转移矩阵特征值和特征向量 对称阵 正交阵 正定阵数据白化矩阵求导 向量对向量求导 标量对向量求导 标量对矩阵求导一.矩阵1.1 SVD奇异值分解&#xff08;Singular Value Decomposition&#xff09;&#xff0c;假设A是一个mn阶矩阵&#xf…

阿里Java编程规约(注释)提炼

【强制】类、类属性、类方法的注释必须使用 Javadoc 规范&#xff0c;使用/**内容*/格式&#xff0c;不得使用 // xxx 方式。 说明&#xff1a;在 IDE 编辑窗口中&#xff0c;Javadoc 方式会提示相关注释&#xff0c;生成 Javadoc 可以正确输出相应注释&#xff1b;在 IDE 中…

Python面试题-交换两个数字的三种方法

Python实现两个数字交换解法1解法2解法3a6 b100 解法1 使用其他变量&#xff0c;最通用的方法 ca ab bc 解法2 不使用其他变量,利算法节省内存空间 aab ba-b aa-b 解法3 python 专有 a,b(b,a) #等号右边是一个元组 或者可以写为&#xff1a; a,bb,a print(a,b)

面试中海量数据处理总结

教你如何迅速秒杀掉&#xff1a;99%的海量数据处理面试题 前言 一般而言&#xff0c;标题含有“秒杀”&#xff0c;“99%”&#xff0c;“史上最全/最强”等词汇的往往都脱不了哗众取宠之嫌&#xff0c;但进一步来讲&#xff0c;如果读者读罢此文&#xff0c;却无任何收获&…

redis——旧版复制

Redis 的复制功能分为同步&#xff08;sync&#xff09;和命令传播&#xff08;command propagate&#xff09;两个操作&#xff1a; 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。命令传播操作用于在主服务器的数据库状态被修改&#xff0c; 导致…

Linux(3)-网-ifconfig,ping,ssh

终端命令网-ping,ssh1. ifconfig -a2. ping3. ssh3.1安装3.2 连接3.3 配置登入别名防火墙端口号,todo1. ifconfig -a 查看IP地址&#xff0c; 还可以用于配置网口。 ifconfig -a 2. ping ping命令&#xff1a; 检测到IP地址的连接是否正常。命令开始后由本机发送数据包a&…

redis——相关问题汇总

什么是redis&#xff1f; Redis 本质上是一个 Key-Value 类型的内存数据库&#xff0c; 整个数据库加载在内存当中进行操作&#xff0c; 定期通过异步操作把数据库数据 flush 到硬盘上进行保存。 因为是纯内存操作&#xff0c; Redis 的性能非常出色&#xff0c; 每秒可以处理…

一文搞定面试中的二叉树问题

一文搞定面试中的二叉树问题 版权所有&#xff0c;转载请注明出处&#xff0c;谢谢&#xff01; http://blog.csdn.net/walkinginthewind/article/details/7518888 树是一种比较重要的数据结构&#xff0c;尤其是二叉树。二叉树是一种特殊的树&#xff0c;在二叉树中每个节点…