网站建设概要设计,页面禁止访问,4k视频素材网站,打开百度浏览器对于redis这样的内存型数据库而言#xff0c;如何删除已过期的数据以及如何在内存满时回收内存是一项很重要的工作。
常见的redis内存回收的工作主要分为两个方面#xff1a;
清理过期的key在内存不足时回收到足够的内存用以存储新的key
清理过期的key
我们很少在redis中…对于redis这样的内存型数据库而言如何删除已过期的数据以及如何在内存满时回收内存是一项很重要的工作。
常见的redis内存回收的工作主要分为两个方面
清理过期的key在内存不足时回收到足够的内存用以存储新的key
清理过期的key
我们很少在redis中使用不带时间戳的key因为那意味着这个key在不久之后有可能会成为死key不为人所知但是又占用了存储空间这样又引出了另一个问题不能由redis直接的去扫描过期的key并删除这样在key的数量达到十万乃至百万级的时候redis就会因为频繁的扫描key而陷入高CPU的不可用状态。
为了清除过期的keyredis设置特殊的策略
惰性删除如其名称这种策略不会主动去删除过期的key而是在客户端试图访问过期的key的时候进行删除定期清除redis有一个定时的策略会不断的抽取部分key来检查是否过期过期的key会被清除
惰性删除
惰性删除是一个降低CPU的有效方法将触发点放到key被访问的时候当key被访问时会触发一个特殊的方法expireIfNeeded这个方法的主要内容如下所示它的作用在于如果key过期那么会在本次访问之后被删除。
int expireIfNeeded(redisDb *db, robj *key) {// 判断 key 是否过期if (!keyIsExpired(db,key)) return 0;....// 如果 server.lazyfree_lazy_expire 为 1 表示异步删除反之同步删除return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :dbSyncDelete(db,key);
}
大致流程如下
客户端访问带时间戳的key服务端检查key是否已经过期如果已经过期那么执行删除操作是否是同步删除取决于参数lazyfree_lazy_expire。返回客户端null如果没有过期返回客户端正常的value值。 定期清除
我们不能期望过期的key总是能够被访问到的我们需要主动清理key的手段。但为了减少清除带来的损耗我们不能直接扫描所有的keyredis选择随机抽取一部分key检查是否已经过期如果已经过期了那么就删除掉。
redis有两种模式用于调用特定方法清理过期的key
定期清理也就是slow模式我们将主要介绍的模式它会定期清理过期的key同时为了减少影响会限制每次执行的总的时间我们可以通过配置hz参数来提高扫描频率默认情况下值为10即每100ms扫描一次。快速清理模式fast模式通过方法beforeSleep执行清理方法
快速清理模式
Fast模式通过于beforeSleep方法执行当满足以下条件之一时将通过Fast模式清理内存中的过期key降低内存压力
上一次任务不是因为超时而退出且已过期键占比近似值server.stat_expired_stale_perc小于可容忍上限config_cycle_acceptable_stale。距离上一次FAST时间未超过指定的时间间隔默认是2000us。
定期清理
其中随机删除的方法是activeExpireCycle位于文件expire.c中随机抽取的key的数量由变量config_keys_per_loop定义它是ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP它本身的值为20通过特定公式计算得出计算公式如下
config_keys_per_loop ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort
effort(值的范围是09默认为0)参数由active_expire_effort得到它表示在抽取key的时候的力度这个值越大意味着每次遍历时抽取的key的数量就越多同样的性能损耗也就越大。
大致流程如下所示
从头遍历所有的库对每一个库先抽取桶再对桶中的key进行遍历如果key已经过期那么删除它同时计数。如果时间已经超过限制直接结束循环如果循环的key的数量已经超过了限制那么继续抽取当前库直至时间达到限制或者过期率降低至期望以下
这个特定值表示在当前数据库中需要抽取的key的数量config_cycle_acceptable_stale的值的大小由公式:
config_cycle_acceptable_staleACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE - effort
计算得到其中ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE的值为10在代码中写死。
再根据下面的公式
do {//抽取key并删除过期key...
} while (sampled 0 || (expired*100/sampled) config_cycle_acceptable_stale);
我们不难发现默认情况下当实际抽取的桶的数量和被释放的key的数量的比值大于10的时候就会认定为当前数据库的过期key的数量过多从而触发再一次的回收。
这里的回收也并不是没有时间限制的为了减少对用户的影响当执行时间超过某个特定值时会直接退出本次收集这个时间由参数timelimit确定这个参数的计算公式为config_cycle_slow_time_perc*1000000/server.hz/100单位为us其中hz由参数CONFIG_DEFAULT_HZ和配置文件共同确定默认为10config_cycle_slow_time_perc由下面的公式确定
config_cycle_slow_time_perc ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 2*effort,
ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC参数用于确定最大的CPU占比默认为25
即默认情况下一次定期扫描不允许超过25ms。
内存已满时淘汰key以回收内存
我们知道如果对redis需要的内存预估错误那么需要写入的时候就有可能会将redis的内存打满我们可以通过配置特定的策略来处理当redis内存满的时候应当怎么做。
可选的策略有以下几种
noeviction(默认策略)对于写请求不再提供服务直接返回错误DEL请求和部分特殊请求除外试图写入新数据时会产生OOM异常allkeys-lru从所有key中使用LRU算法进行淘汰volatile-lru从设置了过期时间的key中使用LRU算法进行淘汰allkeys-random从所有key中随机淘汰数据volatile-random从设置了过期时间的key中随机淘汰volatile-ttl在设置了过期时间的key中根据key的过期时间进行淘汰越早过期的越优先被淘汰volatile-lfuRedis 4.0 后新增的内存淘汰策略淘汰所有设置了过期时间的键值中最少使用的键值allkeys-lfuRedis 4.0 后新增的内存淘汰策略淘汰整个键值中最少使用的键值。
不淘汰-只产生异常
这是默认的选项使用这种策略意味着不会对任意的key进行淘汰但由于内存已满也无法再写入任何新的数据。
看上去会产生一定的异常但是它有它优势它的优势在于不会产生任何意料之外的淘汰操作从而避免热点key意外被淘汰导致出现缓存击穿的现象。
淘汰key回收内存
对于需要淘汰的数据我们可以从数据范围和筛选数据的方法方面选择需要淘汰的key进行淘汰
从数据范围讲根据key是否带时间戳可以分为
从设置了过期时间的key中挑选需要淘汰的key从所有的key中挑选需要淘汰的key
从筛选方法方面根据算法的不同可以分为
随机淘汰key通过LRU算法进行淘汰通过LFU算法进行淘汰
需要注意的是不论是哪种淘汰方法需要淘汰的key的数量都是可以配置的
随机淘汰没什么好说的就是从数据范围中随机取出N个key淘汰掉
近似LRU
严格的LRU算法是需要将所有需要管控的数据都纳入到一个链表中每当有访问的就移动到最前面总是淘汰链表末尾的数据 但是传统的LRU算法也有它的问题
需要用链表管理所有的缓存数据这会带来额外的空间开销当有数据被访问时需要在链表上把该数据移动到头端如果有大量数据被访问就会带来很多链表移动操作会很耗时进而会降低 Redis 缓存性能。
redis选用近似的LRU算法放弃了链表式的结构以此来最大限度的减少算法执行过程中的性能损耗。
redis对象内部维护了一个特殊的字段针对不同的回收策略会有不同的用处对于LRU来讲它是一个24位的时钟记录对象保存到redis的时间对于LFU算法而言它是用来保存访问时间以及频率的字段。
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;
因为不维护字段所以对于需要淘汰的key的选拔是通过在数据范围内随机筛选出N可配置个key选择最旧的一个数据淘汰掉这样做的优势在于能够节省在损耗过程中的性能损耗。
对比严格LRU算法它的优势在于
节省了保存链表的空间降低了频繁移动表节点带来的CPU的损耗
它的问题在于近似始终是近似在效果上必然不如严格LRU算法那么精确。
如下图所示在选取数为10的情况下可以看到是大多数古老的key是已经被淘汰的同时对新的key影响也比较低与严格LRU效果已经接近已经足够满足我们的需求。 近似LFU算法也有LFU算法的固有问题就是LFU实际上只有时间的概念它是没有热度这个说法的也就是说我们在淘汰过程中很可能会淘汰掉一个热点key或者在大批新的数据写入时会影响对旧数据的判断。
LFU算法
我们之前提到redis的对象中维护了一个字段在回收算法是LFU的时候这个字段的作用在于记录最近访问时间以及频率这是一个24位的字段它的前16位用于记录时间后8位则记录频率。 增长策略
counter并不是简单的访问一次就1而是采用了一个0-1之间的p因子控制增长。counter最大值为255。取一个0-1之间的随机数r与p比较当rp时才增加counter这和比特币中控制产出的策略类似。p取决于当前counter值与lfu_log_factor因子counter值与lfu_log_factor因子越大p越小rp的概率也越小counter增长的概率也就越小。
降低策略
既然与热度相关那么自然就会有降低的策略下降的周期也可以通过参数配置配置的含义在于每过多久下降1这样下次计算时可以通过时间差直接计算出当前的key下降后的热度。
下面是LFU算法的热度增长代码
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_factor1);if (r p) counter;return counter;}
可以看到对于任意的key它的热度越高计算得到的允许热度上涨的p就越小热度上升就需要更多次的访问同时还可以通过lfu_log_factor参数来控制增长速率它的值越大增长速度就越小。