zoukankan      html  css  js  c++  java
  • redis之作为缓存的使用(五)缓存污染

    在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。

    如果污染数据很少时,对于系统性能的影响就会很小,但是如果一旦数据量很大时,将会大大占用缓存容量,一旦缓存满的时候,因为保存在缓存中的数据很少使用,我们再往缓存中写入新数据时,就需要先把这些数据逐步淘汰出缓存,因为淘汰需要额外的操作数据库,还有写入缓存开销,这样会影响redis的性能。

    要解决缓存污染,很容易想到解决方案,那就是得把不会再被访问的数据筛选出来并淘汰掉。这样就不用等到缓存被写满以后,再逐一淘汰旧数据之后,才能写入新数据了。而哪些数据能留存在缓存中,是由缓存的淘汰策略决定的。

    在明确知道数据被再次访问的情况下,volatile-ttl 可以有效避免缓存污染。

    lru: LRU 策略的核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据(判断热数据的方式),还会被再次访问。按照这个核心思想,Redis 中的 LRU 策略,会在每个数据对应的 RedisObject 结构体中设置一个 lru 字段,用来记录数据的访问时间戳。在进行数据淘汰时,LRU 策略会在候选数据集中淘汰掉 lru 字段值最小的数据(也就是访问时间最久的数据)。

    但是这种淘汰策略的缺陷是,因为该数据被访问一次就被提到了Mru端,这样的话,但是它之后就不会被使用,这样它从mru端过渡到lru端,需要一定时间,中间会经历一定量的对于其他数据的请求。相当于在使用 LRU 策略淘汰数据时,这些数据会留存在缓存中很长一段时间,造成缓存污染。同时这些数据的一个特性就是只会被访问一次。所以这里就要引入LFU淘汰策略

    LFU 策略中会从两个维度来筛选并淘汰数据:一是,数据访问的时效性(访问时间离当前时间的远近)(这一点与lru相同);二是,数据的被访问次数

    LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把访问时间更久的数据淘汰出缓存。

    为了避免操作链表的开销,Redis 在实现 LRU 策略时使用了两个近似方法:Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳;Redis 并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如 10 个)的数据放入候选集合,后续在候选集合中根据 lru 字段值的大小进行筛选。

    Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分。

    ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;

    counter 值:lru 字段的后 8bit,表示数据的访问次数。

    当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。

    但是8bit最大能表示的值为255,假如A的访问次数为255,而B的访问次数为1024,所以也会被记为255,但是A的最近使用时间大于B的最近使用时间,所以当面临淘汰A,B时会淘汰B

    这样显然和我们预想的不同。

    Redis 也注意到了这个问题。因此,在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则。

    LFU 策略实现的计数规则是:每当数据被访问一次时,首先,用计数器当前的值(即该数据当前被访问的次数)乘以配置项 lfu_log_factor(该值控制计数器值增加的速度,值越大,增加的越慢) 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。使用了这种计算规则后,我们可以通过设置不同的 lfu_log_factor 配置项,来控制计数器值增加的速度,避免 counter 值很快就到 255 了。

    现在有一种特殊情况:
    在一些场景下,有些数据在短时间内被大量访问后就不会再被访问了。那么再按照访问次数来筛选的话,这些数据会被留存在缓存中,但不会提升缓存命中率。为此,Redis 在实现 LFU 策略时,还设计了一个 counter 值的衰减机制(较少counter值)。

    LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位(精度为分钟)。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。

    假设 lfu_decay_time 取值为 1,如果数据在 N 分钟内没有被访问,那么它的访问次数就要减 N。如果 lfu_decay_time 取值更大,那么相应的衰减值会变小,衰减效果也会减弱。所以,如果业务应用中有短时高频访问的数据的话,建议把 lfu_decay_time 值设置为 1,这样一来,LFU 策略在它们不再被访问后,会较快地衰减它们的访问次数,尽早把它们从缓存中淘汰出去,避免缓存污染。

    使用了 LFU 策略后,缓存还会被污染吗?

    我觉得还是有被污染的可能性,被污染的概率取决于LFU的配置,也就是lfu-log-factor和lfu-decay-time参数。

    1、根据LRU counter计数规则可以得出,counter递增的概率取决于2个因素:

    a) counter值越大,递增概率越低
    b) lfu-log-factor设置越大,递增概率越低

    所以当访问次数counter越来越大时,或者lfu-log-factor参数配置过大时,counter递增的概率都会越来越低,这种情况下可能会导致一些key虽然访问次数较高,但是counter值却递增困难,进而导致这些访问频次较高的key却优先被淘汰掉了。

    另外由于counter在递增时,有随机数比较的逻辑,这也会存在一定概率导致访问频次低的key的counter反而大于访问频次高的key的counter情况出现。

    2、如果lfu-decay-time配置过大,则counter衰减会变慢,也会导致数据淘汰发生推迟的情况。

    3、另外,由于LRU的ldt字段只采用了16位存储,其精度是分钟级别的,在counter衰减时可能会产生同一分钟内,后访问的key比先访问的key的counter值优先衰减(原因可能是在同一分钟内被访问的数据,经过计算可能它们之间按照分钟计算出来之后的精度可能差一分钟),进而先被淘汰掉的情况。

  • 相关阅读:
    SpringBoot和SpringCould的关系
    MyBatis全局配置文件头
    MyBatis的SQL映射文件头
    MyBatis 驼峰式配置 yml配置
    频率组件
    序列化和反序列化
    生成器面试题
    序列化组件
    进程间通信IPC机制
    信号量、event事件和线程queue
  • 原文地址:https://www.cnblogs.com/foreverlearnxzw/p/13848160.html
Copyright © 2011-2022 走看看