当前位置: 首页 > 科技观察

不要混淆Redis的内存淘汰策略和过期删除策略!

时间:2023-03-12 12:08:02 科技观察

大家好,我是小林。Redis的“内存淘汰策略”和“过期删除策略”很容易被很多朋友混淆。虽然这两种机制都执行删除操作,但触发条件和使用的策略不同。今天和大家聊聊“内存淘汰策略”和“过期删除策略”。我们走吧!过期删除策略Redis可以为key设置过期时间,所以需要有相应的机制来删除过期的键值对,过期键值删除策略就是用来做这个工作的。如何设置过期时间?先说一下设置key过期时间的命令。设置key过期时间的命令有4条:expire:设置key在n秒后过期,例如expirekey100表示??设置key在100秒后过期;pexpire:设置key在n毫秒后过期,如pexpirekey2100000表示设置key2在100000毫秒(100秒)后过期。expireat:设置key在某个时间戳(精确到秒)后过期,例如expireatkey31655654400表示key3在时间戳1655654400(精确到秒)后过期;pexpireat:设置key在某个时间戳(精确到毫秒)后过期,例如pexpireatkey41655654400000表示key4在时间戳1655654400000(精确到毫秒)后过期。当然,在设置字符串的时候,也可以同时设置key的过期时间,一共有3种命令类型:setex:设置key-value时pair,指定过期时间(精确到秒);setpx:设置键值对时,指定过期时间(精确到毫秒);setex:设置键值对时,指定过期时间(精确到秒)。如果要查看某个键的剩余生存时间,可以使用TTL命令。#设置键值对时,同时指定一个60秒的过期时间>setexkey160value1OK#查看key1过期时间还剩多少时间>ttlkey1(integer)56>ttlkey1(integer)52如果突然后悔,取消key的过期时间,可以使用PERSIST命令。#取消key1的过期时间>persistkey1(integer)1#使用persist命令后,#查看key1的生存时间,结果为-1,说明key1永不过期>ttlkey1(integer)-1如何判断key已经过期了?每当我们给一个key设置了过期时间,Redis就会把这个key和过期时间保存在一个expires字典(expiresdict)中,也就是说“过期字典”保存了数据库中所有key的过期时间。过期字典存放在redisDb结构中,如下:typedefstructredisDb{dict*dict;/*数据库键空间,存放所有的键值对*/dict*expires;/*key过期时间*/....}redisDb;过期字典的数据结构如下:过期字典的key是一个指向key对象的指针;expireddictionary的值为longlong类型的整数,存储key的过期时间;过期字典的数据结构如下图所示:字典实际上是一个哈希表。哈希表最大的好处就是我们可以用O(1)的时间复杂度来快速查找。当我们查询一个key时,Redis首先会检查该key是否存在于过期字典中:如果不存在,则正常读取key值;如果存在,则获取key的过期时间,然后与当前系统时间进行比较,如果大于系统时间,则没有过期,否则判定key过期。过期键判断流程如下图所示:过期删除策略有哪些?在说Redis的过期删除策略之前,先给大家介绍三种常见的过期删除策略:定期删除;惰性删除;定期删除;接下来,分别分析它们的优缺点。什么是定时删除策略?定时删除策略的方法是在设置key过期时间的同时创建一个定时事件。时间一到,事件处理器会自动执行key的删除操作。定时删除策略的优点:可以保证过期的key尽快被删除,也就是尽快释放内存。因此,定期删除对记忆是最友好的。定时删除策略的缺点:当过期key较多时,删除过期key可能会占用相当一部分CPU时间。当内存不紧张但CPU时间紧张时,CP??U时间用于删除与当前任务无关的过期项。在key上,无疑会对服务器的响应时间和吞吐量产生影响。因此,定时删除策略对CPU并不友好。什么是惰性删除策略?惰性删除策略的做法是不主动删除过期的key。每次从数据库访问key时,都会检查key是否过期,如果过期则删除key。惰性删除策略的优点:因为每次访问key都会检查过期时间,所以这种策略占用的系统资源非常少。因此,惰性删除策略对CPU时间是最友好的。惰性删除策略的缺点:如果一个key已经过期,而这个key还在数据库中,只要这个过期的key还没有被访问,它占用的内存就不会被释放,造成一定的内存空间浪费.因此,惰性删除策略对内存并不友好。定期删除政策是什么样的?定时删除策略是每隔一段时间“随机”从数据库中取出一定数量的key进行检查,将过期的key删除。周期删除策略的优点:通过限制删除操作的时长和频率,可以减少删除操作对CPU的影响,同时也可以删除一些过期的数据,减少过期key占用的无效空间。定时删除策略的缺点:内存清理不如定时删除有效,懒删除占用的系统资源少。难以确定执行删除操作的时间和频率。如果执行过于频繁,周期性删除策略就变得和定时删除策略一样,对CPU不友好;如果执行的太少,就和懒删除一样,过期键占用的内存不会及时释放。Redis过期删除策略是什么?上面介绍了三种过期删除策略,各有优缺点,仅使用某一种策略是不能满足实际需要的。因此,Redis选择了“惰性删除+定时删除”两种策略组合,在合理使用CPU时间和避免内存浪费之间取得平衡。Redis是如何实现惰性删除的?Redis的惰性删除策略是通过db.c文件中的expireIfNeeded函数实现的。代码如下:intexpireIfNeeded(redisDb*db,robj*key){//判断key是否过期if(!keyIsExpired(db,key))return0;..../*Deleteexpiredkey*/....//如果server.lazyfree_lazy_expire为1,表示异步删除,否则同步删除;返回server.lazyfree_lazy_expire?dbAsyncDelete(db,key):dbSyncDelete(db,key);}Redis在访问或修改key之前会调用expireIfNeeded函数检查key是否过期。开始提供参数),然后返回null给客服;如果没有过期,则什么都不做,然后将正常的键值对返回给客户端;懒删除流程图如下:Redis是如何实现定时删除的?再回想一下,定时删除策略的方法:每隔一段时间“随机”从数据库中取出一定数量的key进行检查,将过期的key删除。1、检查间隔多久?在Redis中,默认情况下,数据库每秒检查10次是否过期。这个配置可以通过Redis配置文件redis.conf来配置。配置key为hz,默认值为hz10。特别强调的是,每次检查数据库时,不会遍历过期字典中的所有key,而是从数据库中随机选择一定数量的key进行过期检查。2、抽查次数是多少?我查看了源码,定时删除的实现是在expire.c文件下的activeExpireCycle函数中。随机检查次数由ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP定义,硬编码在代码中,取值为20。也就是说,在数据库的每一轮抽查中,随机抽取20个key来判断是否过期。接下来详细说一下Redis的定时删除过程:从过期字典中随机抽取20个key;检查20个key是否过期,删除过期key;如果本轮检查的过期key数量超过5(20/4),即“过期key数量”占“随机选择key数量”的25%以上,则继续重复步骤1;如果过期key的比例低于25%,则停止并继续删除Expirekey,然后等待下一轮再次检查。可见,定时删除是一个循环过程。为了保证定时删除不会过度循环导致线程卡死,Redis增加了定时删除循环过程的时间限制,默认不会超过25ms。对于定时删除的过程,我写了一个伪代码:do{//过期数量expired=0;//随机抽取数量num=20;while(num--){//1.从过期字典中随机抽取一个key//2。判断key是否过期,过期删除,同时检查expired++}//超时退出if(timelimit_exit)return;/*如果本轮检查的过期key超过25%,则继续随机检查,否则退出本轮检查*/}while(expired>20/4);定时删除的过程如下:前面提到的内存淘汰策略中的过期删除策略就是删除过期的key,当Redis的运行内存超过Redis设置的最大内存时,就会使用内存淘汰策略来删除删除符合条件的键,保证Redis的高效运行。如何设置Redis的最大运行内存?在配置文件redis.conf中,可以通过参数maxmemory设置最大运行内存。只有当Redis的运行内存达到我们设置的最大运行内存时,才会触发内存淘汰策略。不同位数的操作系统,maxmemory的默认值是不同的:在64位操作系统中,maxmemory的默认值为0,也就是说没有内存大小限制,所以无论用户在Redis中存储多少数据,Redis不会检查Availablememory并且什么都不做,直到Redis实例因内存不足而崩溃。在32位操作系统中,maxmemory默认值为3G,因为32位机器最多只支持4GB内存,而系统本身需要一定的内存资源来支持运行,所以32位操作系统系统将最大可用内存限制为3GB。内存非常合理,可以避免Redis实例因为内存不足而崩溃。Redis内存淘汰策略有哪些?Redis内存淘汰策略一共有八种,大致分为“无数据淘汰”和“有数据淘汰”两种。1.Noevictionstrategyfornodataelimination(Redis3.0后默认内存淘汰策略):表示当运行内存超过设置的最大内存时,不淘汰数据,但不提供服务,报错会直接退回。2.数据淘汰策略“数据淘汰”策略又可以细分为“在设置过期时间的数据中淘汰”和“在所有数据范围内淘汰”两种策略。淘汰有过期时间的数据:volatile-random:随机淘汰任意一个设置过期时间的key值;volatile-ttl:优先消除较早过期的键值。volatile-lru(Redis3.0之前,默认的内存淘汰策略):淘汰所有设置过期时间的键值中最长未使用的键值;volatile-lfu(Redis4.0后新的内存淘汰策略):淘汰所有设置过期时间的键值中,最少使用的键值;在所有数据范围内消除:allkeys-random:随机消除任意键值;allkeys-lru:剔除整个键值中最长未使用的键值;allkeys-lfu(Redis4.0后新的内存淘汰策略):淘汰整个键值中最少使用的键值。如何查看Redis当前使用的内存淘汰策略?可以使用configgetmaxmemory-policy命令查看当前Redis的内存淘汰策略。命令如下:127.0.0.1:6379>configgetmaxmemory-policy1)"maxmemory-policy"2)"noeviction"可以看出目前Redis使用的是noeviction类型的内存淘汰策略,即Redis3.0之后默认使用的内存淘汰策略。意思是当运行内存超过设置的最大内存时,不会剔除任何数据,但会报错进行新的操作。如何修改Redis的内存淘汰策略?设置内存淘汰策略的方法有两种:方法一:通过“configsetmaxmemory-policy”命令设置。它的好处是设置后立即生效,不需要重启Redis服务。缺点是重启Redis后设置会失效。方法二:修改Redis配置文件,设置“maxmemory-policy”。它的好处是重启Redis服务后配置不会丢失。缺点是必须重启Redis服务才能使设置生效。LRU算法和LFU算法有什么区别?LFU内存淘汰算法是Redis4.0之后新增的内存淘汰策略,为什么要加入这个算法呢?那肯定是要解决LRU算法的问题。接下来我们看看这两种算法有什么区别?Redis是如何实现这两种算法的呢?什么是LRU算法?LRU全称是LeastRecentlyUsed,译为最近最少使用,会选择剔除最近最少使用的数据。传统的LRU算法的实现是基于“链表”结构的。链表中的元素按照操作顺序从前到后排列。新操作的键将被移动到列表的头部。当需要内存淘汰时,只需要删除链表末尾的元素即可,因为链表末尾的元素代表最长时间没有被使用的元素。Redis并没有这样实现LRU算法,因为传统的LRU算法有两个问题:需要用一个链表来管理所有的缓存数据,会带来额外的空间开销;访问数据时,需要将存储在链表上的数据移动到首端,如果访问的数据量很大,会带来大量的链表移动操作,耗时耗力会降低Redis缓存的性能。Redis是如何实现LRU算法的?Redis实现了一种近似的LRU算法,可以更好的节省内存。它的实现是在Redis的对象结构中增加一个字段来记录这条数据的最后访问时间。Redis在进行内存淘汰时,采用随机采样的方式淘汰数据。它随机选择5个值(这个值是可配置的),然后剔除最长时间没有被使用的那个。Redis实现的LRU算法的优点:不需要为所有数据维护一个很大的链表,节省空间;不需要每次访问数据时都移动链表项,提高了缓存的性能;但是LRU算法有一个问题无法解决缓存污染问题,比如应用一次读取大量数据,而这些数据只会在这一次被读取,那么这些数据会在Redis缓存中保留一段时间时间长了,造成缓存污染。所以在Redis4.0之后引入了LFU算法来解决这个问题。LFU算法是什么?LFU的全称是LeastFrequentlyUsed,译为最近最少使用。LFU算法根据数据访问次数来剔除数据。它的核心思想是“如果数据在过去被访问过多次,那么将来会被更频繁地访问”。.因此LFU算法会记录每条数据的访问次数。当再次访问一条数据时,对该数据的访问次数增加。这样就解决了数据偶尔被访问一次后,会长时间保留在缓存中的问题,比LRU算法更合理。Redis是如何实现LFU算法的?与LRU算法的实现相比,LFU算法记录了更多关于“数据访问频率”的信息。Redis对象的结构如下:typedefstructredisObject{...//24位,用于记录对象的访问信息unsignedlru:24;...}抢劫;Redis对象头中的lru字段,在LRU算法和LFU算法下使用不同。在LRU算法中,Redis对象头的24位lru字段用于记录key的访问时间戳,所以在LRU模式下,Redis可以根据lru字段中记录的值来比较最后一次key访问objectheader需要很长时间才能淘汰最久未使用的key。在LFU算法中,Redis对象头的24位lru字段分为两段存储,高16位存储ldt(LastDecrementTime),低8位存储logc(LogisticCounter)。ldt用于记录key的访问时间戳;logc用于记录key的访问频率。它的值越小,使用频率越低,越容易被淘汰。每个新添加的key的logc初始值为5,注意logc不是简单的访问次数,而是访问频率(visitfrequency),因为logc会随着时间衰减。每次访问key时,都会先对logc进行一次衰减操作。衰减值与前后访问时间的差距有关。如果上次访问时间和本次访问时间的时间差很大,衰减值就会越大,这种方式实现的LFU算法根据访问频率来剔除数据,而不仅仅是访问次数。访问频率需要考虑密钥访问发生的时间。key的上一次访问距离当前时间越长,该key被访问的频率越低,被淘汰的概率就越大。对logc进行衰减操作后,开始对logc进行增量操作。增加操作不是简单的直接+1,而是按照概率增加。如果logc大于key,则其logc更难增加。因此,当Redis访问key时,logc是这样变化的:首先,logc根据上次访问的当前时间衰减;然后,logc的值按照一定的概率增加。redis.conf提供了两个配置项,用于调整LFU算法来控制logc的增长和衰减:lfu-decay-time用于调整logc的衰减速度,是一个以分钟为单位的值,默认值为1,lfu-decay-time的值越大,衰减越慢;lfu-log-factor用于调整logc的增长速度,lfu-log-factor的值越大,logc增长越慢。总结一下,Redis采用的过期删除策略是“惰性删除+定时删除”,删除的对象是过期键。内存淘汰策略就是解决内存过剩的问题。当Redis的运行内存超过最大运行内存时,就会触发内存淘汰策略。Redis4.0之后一共实现了8种内存淘汰策略。分类,如下: