Redis的LRU缓存淘汰算法实现

Redis的LRU缓存淘汰算法实现,第1张

LRU, 最近最少使用 (Least Recently Used,LRU),经典缓存算法。

LRU会使用一个链表维护缓存中每个数据的访问情况,并根据数据的实时访问,调整数据在链表中的位置,然后通过数据在链表中的位置,表示数据是最近刚访问的,还是已有段时间未访问。

LRU会把链头、尾分别设为MRU端和LRU端:

LRU可分成如下情况:

case2图解:链表长度为5,从链表头部到尾部保存的数据分别是5,33,9,10,8。假设数据9被访问一次,则9就会被移动到链表头部,同时,数据5和33都要向链表尾部移动一位。

所以若严格按LRU实现,假设Redis保存的数据较多,还要在代码中实现:

最终导致降低Redis访问性能。

所以,无论是为节省内存 or 保持Redis高性能,Redis并未严格按LRU基本原理实现,而是 提供了一个近似LRU算法实现

Redis的内存淘汰机制是如何启用近似LRU算法的?redisconf中的如下配置参数:

所以,一旦设定maxmemory选项,且将maxmemory-policy配为allkeys-lru或volatile-lru,近似LRU就被启用。allkeys-lru和volatile-lru都会使用近似LRU淘汰数据,区别在于:

Redis如何实现近似LRU算法的呢?

近似LRU算法仍需区分不同数据的访问时效性,即Redis需知道数据的最近一次访问时间。因此,有了LRU时钟:记录数据每次访问的时间戳。

Redis对每个KV对中的V,会使用个redisObject结构体保存指向V的指针。那redisObject除记录值的指针,还会使用24 bits保存LRU时钟信息,对应的是lru成员变量。这样,每个KV对都会把它最近一次被访问的时间戳,记录在lru变量。

redisObject定义包含lru成员变量的定义:

每个KV对的LRU时钟值是如何计算的?Redis Server使用一个实例级别的全局LRU时钟,每个KV对的LRU time会根据全局LRU时钟进行设置。

这全局LRU时钟保存在Redis全局变量server的成员变量 lruclock

当Redis Server启动后,调用initServerConfig初始化各项参数时,会调用getLRUClock设置lruclock的值:

于是,就得注意, 若一个数据前后两次访问的时间间隔 1s,那这两次访问的时间戳就是一样的! 因为LRU时钟精度就是1s,它无法区分间隔小于1秒的不同时间戳!

getLRUClock函数将获得的UNIX时间戳,除以LRU_CLOCK_RESOLUTION后,就得到了以LRU时钟精度来计算的UNIX时间戳,也就是当前的LRU时钟值。

getLRUClock会把LRU时钟值和宏定义LRU_CLOCK_MAX(LRU时钟能表示的最大值)做与运算。

所以默认情况下,全局LRU时钟值是以1s为精度计算得UNIX时间戳,且是在initServerConfig中进行的初始化。

那Redis Server运行过程中,全局LRU时钟值是如何更新的?和Redis Server在事件驱动框架中,定期运行的时间事件所对应的serverCron有关。

serverCron作为时间事件的回调函数,本身会周期性执行,其频率值由redisconf的 hz配置项 决定,默认值10,即serverCron函数会每100ms(1s/10 = 100ms)运行一次。serverCron中,全局LRU时钟值就会按该函数执行频率,定期调用getLRUClock进行更新:

这样,每个KV对就能从全局LRU时钟获取最新访问时间戳。

对于每个KV对,它对应的redisObjectlru在哪些函数进行初始化和更新的呢?

对于一个KV对,其LRU时钟值最初是在这KV对被创建时,进行初始化设置的,这初始化 *** 作在 createObject函数 中调用,当Redis要创建一个KV对,就会调用该函数。

createObject除了会给redisObject分配内存空间,还会根据maxmemory_policy配置,初始化设置redisObjectlru。

LRU_CLOCK返回当前全局LRU时钟值。因为一个KV对一旦被创建,就相当于有了次访问,其对应LRU时钟值就表示了它的访问时间戳:

那一个KV对的LRU时钟值又是何时再被更新?

只要一个KV对被访问,其LRU时钟值就会被更新!而当一个KV对被访问时,访问 *** 作最终都会调用 lookupKey

lookupKey会从全局哈希表中查找要访问的KV对。若该KV对存在,则lookupKey会根据maxmemory_policy的配置值,来更新键值对的LRU时钟值,也就是它的访问时间戳。

而当maxmemory_policy没有配置为LFU策略时,lookupKey函数就会调用LRU_CLOCK函数,来获取当前的全局LRU时钟值,并将其赋值给键值对的redisObject结构体中的lru变量

这样,每个KV对一旦被访问,就能获得最新的访问时间戳。但你可能好奇:这些访问时间戳最终是如何被用于近似LRU算法进行数据淘汰的?

Redis之所以实现近似LRU,是为减少内存资源和 *** 作时间上的开销。

近似LRU主要逻辑在performEvictions。

performEvictions被evictionTimeProc调用,而evictionTimeProc函数又是被processCommand调用。

processCommand,Redis处理每个命令时都会调用:

然后,isSafeToPerformEvictions还会再次根据如下条件判断是否继续执行performEvictions:

一旦performEvictions被调用,且maxmemory-policy被设置为allkeys-lru或volatile-lru,近似LRU就被触发执行了。

执行可分成如下步骤:

调用getMaxmemoryState评估当前内存使用情况,判断当前Redis Server使用内存容量是否超过maxmemory配置值。

若未超过maxmemory ,则返回C_OK,performEvictions也会直接返回。

getMaxmemoryState评估当前内存使用情况的时候,若发现已用内存超出maxmemory,会计算需释放的内存量。这个释放内存大小=已使用内存量-maxmemory。

但已使用内存量并不包括用于主从复制的复制缓冲区大小,这是getMaxmemoryState通过调用freeMemoryGetNotCountedMemory计算的。

而若当前Server使用的内存量超出maxmemory上限 ,则performEvictions会执行while循环淘汰数据释放内存。

为淘汰数据,Redis定义数组EvictionPoolLRU,保存待淘汰的候选KV对,元素类型是evictionPoolEntry结构体,保存了待淘汰KV对的空闲时间idle、对应K等信息:

这样,Redis Server在执行initSever进行初始化时,会调用evictionPoolAlloc为EvictionPoolLRU数组分配内存空间,该数组大小由EVPOOL_SIZE决定,默认可保存16个待淘汰的候选KV对。

performEvictions在淘汰数据的循环流程中,就会更新这个待淘汰的候选KV对集合,即EvictionPoolLRU数组。

performEvictions调用evictionPoolPopulate,其会先调用dictGetSomeKeys,从待采样哈希表随机获取一定数量K:

于是,dictGetSomeKeys返回采样的KV对集合。evictionPoolPopulate根据实际采样到的KV对数量count,执行循环:调用estimateObjectIdleTime计算在采样集合中的每一个KV对的空闲时间:

接着,evictionPoolPopulate遍历待淘汰的候选KV对集合,即EvictionPoolLRU数组,尝试把采样的每个KV对插入EvictionPoolLRU数组,取决于如下条件之一:

有一成立,evictionPoolPopulate就能把采样KV对插入EvictionPoolLRU数组。等所有采样键值对都处理完后,evictionPoolPopulate函数就完成对待淘汰候选键值对集合的更新了。

接下来,performEvictions开始选择最终被淘汰的KV对。

因evictionPoolPopulate已更新EvictionPoolLRU数组,且该数组里的K,是按空闲时间从小到大排好序了。所以,performEvictions遍历一次EvictionPoolLRU数组,从数组的最后一个K开始选择,若选到的K非空,就把它作为最终淘汰的K。

该过程执行逻辑:

一旦选到被淘汰的K,performEvictions就会根据Redis server的惰性删除配置,执行同步删除或异步删除:

至此,performEvictions就淘汰了一个K。若此时释放的内存空间还不够,即没有达到待释放空间,则performEvictions还会 重复执行 前面所说的更新待淘汰候选KV对集合、选择最终淘汰K的过程,直到满足待释放空间的大小要求。

performEvictions流程:

近似LRU算法并未使用耗时且耗空间的链表,而使用 固定大小的待淘汰数据集合 ,每次随机选择一些K加入待淘汰数据集合。

最后,按待淘汰集合中K的空闲时间长度,删除空闲时间最长的K。

根据LRU算法的基本原理,发现若严格按基本原理实现LRU算法,则开发的系统就需要额外内存空间保存LRU链表,系统运行时也会受到LRU链表 *** 作的开销影响。

而Redis的内存资源和性能都很重要,所以Redis实现近似LRU算法:

一个算法的基本原理和算法的实际执行,在系统开发中会有一定折中,需综合考虑所开发的系统,在资源和性能方面的要求,以避免严格按照算法实现带来的资源和性能开销。

redis-cli --scan  ,306版本,不知道低版本的有没有这个参数。

如果想要 所有键值对

redis-cli --scan |while read a;do echo -n $a: ;redis-cli get $a ;done

-r(repeat)选项代表将命令执行多次,例如下面 *** 作将会执行三次ping

命令:

redis-cli -r 3 ping 

PONG

PONG 

PONG

-i(interval)选项代表每隔几秒执行一次命令,但是-i选项必须和-r选 项一起使用,下面的 *** 作会每隔1秒执行一次ping命令,一共执行5次:

注意-i的单位是秒,不支持毫秒为单位,但是如果想以每隔10毫秒执行 一次,可以用-i001

redis-cli -r 5 -i 001 ping

例如下面的 *** 作利用-r和-i选项,每隔1秒输出内存的使用量,一共输出 100次

redis-cli -r 100 -i 1 info | grep used_memory_human 

used_memory_human:295G 

used_memory_human:295G

-x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参 数,例如下面的 *** 作会将字符串world作为set hello的值

$ echo "world" | redis-cli -x set hello 

OK

-c(cluster)选项是连接Redis Cluster节点时需要使用的,-c选项可以防 止moved和ask异常

如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要 手动输入auth命令

--scan选项和--pattern选项用于扫描指定模式的键,相当于使用scan命令

--slave选项是把当前客户端模拟成当前Redis节点的从节点,可以用来 获取当前Redis节点的更新 *** 作

下面开启第一个客户端,使用--slave选项,看到同步已完成:

$ redis-cli --slave 

SYNC with master, discarding 72 bytes of bulk transfer 

SYNC done Logging commands from master

--rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。 可使用它做持久化文件的定期备份

--pipe选项用于将命令封装成Redis通信协议定义的数据格式,批量发送 给Redis执行

例如下面 *** 作 同时执行了set hello world和incr counter两条命令:

echo -en '3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n2\r\n$4\r\nincr\r\ n$7\r\ncounter\r\n' | redis-cli --pipe

--bigkeys选项使用scan命令对Redis的键进行采样,从中找到内存占用比

较大的键值,这些键可能是系统的瓶颈

--eval选项用于执行指定Lua脚本,有关Lua脚本的使用将在34节介绍。

latency有三个选项,分别是--latency、--latency-history、--latency-dist。 它们都可以检测网络延迟,对于Redis的开发和运维非常有帮助。

该选项可以测试客户端到目标Redis的网络延迟,例如当前拓扑结构如 图3-4所示。客户端B和Redis在机房B,客户端A在机房A,机房A和机房B是

跨地区的

客户端B:

redis-cli -h {machineB} --latency 

min: 0, max: 1, avg: 007 (4211 samples)

客户端A:

redis-cli -h {machineB} --latency 

min: 0, max: 2, avg: 104 (2096 samples)

可以看到客户端A由于距离Redis比较远,平均网络延迟会稍微高一些

--latency的执行结果只有一条,如果想以分时段的形式了解延迟信息, 可以使用--latency-history选项:

redis-cli -h 1010xxxx --latency-history 

min: 0, max: 1, avg: 028 (1330 samples) -- 1501 seconds range… 

min: 0, max: 1, avg: 005 (1364 samples) -- 1501 seconds range

可以看到延时信息每15秒输出一次,可以通过-i参数控制间隔时间。

(3)--latency-dist

该选项会使用统计图表的形式从控制台输出延迟统计信息。

--stat选项可以实时获取Redis的重要统计信息,虽然info命令中的统计信 息更全,但是能实时看到一些增量的数据(例如requests)对于Redis的运维还是有一定帮助的

--no-raw选项是要求命令的返回结果必须是原始的格式,--raw恰恰相反,返回格式化后的结果。

在Redis中设置一个中文的value:

$redis-cli set hello "你好" 

OK

如果正常执行get或者使用--no-raw选项,那么返回的结果是二进制格式:

如果使用了--raw选项,将会返回中文:

$redis-cli --raw get hello

你好

redis不重复取出思路如下:(1)、后台有一个单独获取token接口 A,token是一个随机唯一字符串,可以是uuid,每次调用获取(token也可以前端生成,减少一次前后台交互);

还有一个提交业务数据的接口B(就是为了防止这个接口B,重复 *** 作提交);

(2)、在进入需要提交页面时,调用后台接口A,例如获取token = 123,并且返回给前端token

(3)、前端点击提交按钮时,只调用提交接口B,此处不能再次调用获取token接口A,提交业务数据,并且把之前接口A获取的token = 123 带到后台过去,

需要保证在当前页面重复点击过来传递的token是同一个值;

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

集合的数据是唯一的,插入之后再次插入时不会执行

(1)查询集合里面元素的数量

(2)从集合中获取数据

--如果count省略则随机获取一条数据

--如果count为其他大于1的整数,则获取对应条数据

--如果count对应的证书大于集合总数据的条数,则获取集合所有数据

(3)获取集合中的所有数据

(4)判断集合中是否存在某个元素

--如果数据存在,则返回1,如果数据不存在,则返回0

既属于A集合又属于B/其他集合,集合数量可以是多个,多个代表对应所有集合的交集

A集合与B/其他集合所包含的所有数据,如果数据一样则去重,集合数量可以是多个,多个代表对应所有集合的并集

只属于A(key1)集合,不属于其他集合的数据

锁要实现的三个目标:

该命令仅在密钥尚不存在时才设置key(NX选项),到期时间为30000毫秒(PX选项)。键设置为值 “myrandomvalue” 。此值必须在所有客户端和所有锁定请求中都是唯一的。

使用随机值是为了以安全的方式释放锁,实现上使用了Redis的脚本:只有当key存在且其值恰好是我期望的随机值时才删除key。这是通过以下Lua脚本完成的:

这一点很重要,这可以避免删除由另一个客户端创建的锁。例如:

ok,这样我们完成redis分布式锁的实现。你以为这样就安全了吗,接下来我们设想这样一个情况:

首先,假设我们的Redis是主从的:

这时就会同时有两把锁存在的情况。对于这种情况(非常特殊),Redis官方提供了一个叫RedLock的解决方案。

我们假设我们有N个Redis主机。这些节点完全独立,因此我们不使用复制或任何其他隐式协调系统。我们假设N = 5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行5个Redis主服务器,以确保它们以大多数独立的方式失败。

为了获取锁,客户端执行以下 *** 作:

我个人觉得对于小公司这还是相当昂贵的。

其他的细节请参考 redis官方文档 。

以上就是关于Redis的LRU缓存淘汰算法实现全部的内容,包括:Redis的LRU缓存淘汰算法实现、有没有好的方法遍历redis里面的所有key、Redis-cli详解等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/9579184.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-29
下一篇2023-04-29

发表评论

登录后才能评论

评论列表(0条)

    保存