Redis 对过时数据的处理

Redis 对过时数据的处理redis

在 redis 中,对于已通过期的数据,Redis 采用两种策略来处理这些数据,分别是惰性删除和按期删除算法

惰性删除服务器

惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过时,若是过时则执行删除并返回 null 给客户端,若是没有过时则返回正常信息给客户端。微信

它的优势是简单,不须要对过时的数据作额外的处理,只有在每次访问的时候才会检查键值是否过时,缺点是删除过时键不及时,形成了必定的空间浪费。数据结构

源码dom

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

    if (expireIfNeeded(db,key) == 1) {
        /* Key expired. If we are in the context of a master, expireIfNeeded()
         * returns 0 only when the key does not exist at all, so it's safe
         * to return NULL ASAP. */
        if (server.masterhost == NULL) {
            server.stat_keyspace_misses++;
            notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
            return NULL;
        }

        /* However if we are in the context of a slave, expireIfNeeded() will
         * not really try to expire the key, it only returns information
         * about the "logical" status of the key: key expiring is up to the
         * master in order to have a consistent view of master's data set.
         *
         * However, if the command caller is not the master, and as additional
         * safety measure, the command invoked is a read-only command, we can
         * safely return NULL here, and provide a more consistent behavior
         * to clients accessign expired values in a read-only fashion, that
         * will say the key as non existing.
         *
         * Notably this covers GETs when slaves are used to scale reads. */
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            server.stat_keyspace_misses++;
            notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
            return NULL;
        }
    }
    val = lookupKey(db,key,flags);
    if (val == NULL) {
        server.stat_keyspace_misses++;
        notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
    }
    else
        server.stat_keyspace_hits++;
    return val;
}

按期删除ide

按期删除:Redis会周期性的随机测试一批设置了过时时间的key并进行处理。测试到的已过时的key将被删除。测试

具体的算法以下:ui

  • Redis配置项hz定义了serverCron任务的执行周期,默认为10,表明了每秒执行10次;this

  • 每次过时key清理的时间不超过CPU时间的25%,好比hz默认为10,则一次清理时间最大为25ms;

  • 清理时依次遍历全部的db;

  • 从db中随机取20个key,判断是否过时,若过时,则逐出;

  • 如有5个以上key过时,则重复步骤4,不然遍历下一个db;

  • 在清理过程当中,若达到了25%CPU时间,退出清理过程;

虽然redis的确是不断的删除一些过时数据,可是不少没有设置过时时间的数据也会愈来愈多,那么redis内存不够用的时候是怎么处理的呢?这里咱们就会谈到淘汰策略

Redis内存淘汰策略

当redis的内存超过最大容许的内存以后,Redis会触发内存淘汰策略,删除一些不经常使用的数据,以保证redis服务器的正常运行

在redis 4.0之前,redis的内存淘汰策略有如下6种

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:加入键的时候,若是过限,首先经过LRU算法驱逐最久没有使用的键
  • volatile-lru:加入键的时候若是过限,首先从设置了过时时间的键集合中驱逐最久没有使用的键
  • allkeys-random:加入键的时候若是过限,从全部key随机删除
  • volatile-random:加入键的时候若是过限,从过时键的集合中随机驱逐
  • volatile-ttl:从配置了过时时间的键中驱逐立刻就要过时的键
    在redis 4.0之后,又增长了如下两种
  • volatile-lfu:从全部配置了过时时间的键中驱逐使用频率最少的键
  • allkeys-lfu:从全部键中驱逐使用频率最少的键

内存淘汰策略能够经过配置文件来修改,redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction

LRU(the least recently used 最近最少使用)算法

若是一个数据在最近没有被访问到,那么在将来被访问的可能性也很小,所以当空间满的时候,最久没有被访问的数据最早被置换(淘汰)

LRU算法一般经过双向链表来实现,添加元素的时候,直接插入表头,访问元素的时候,先判断元素是否在链表中存在,若是存在就把该元素移动至表头,因此链表的元素排列顺序就是元素最近被访问的顺序,当内存达到设置阈值时,LRU队尾的元素因为被访问的时间线较远,会优先踢出
file

file

可是在redis中,并无严格实行LRU算法,之因此这样是由于LRU须要消耗大量的额外内存,须要对现有的数据结构进行较大的改造,近似LRU算法采用在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和LRU算法很是近似的效果。Redis的 LRU算法给每一个key增长了一个额外的长度为24bit的小字段,记录最后一次被访问的时间戳。

redis经过maxmemory-samples 5配置,对key进行采样淘汰。同时在Redis3.0之后添加了淘汰池进一步提高了淘汰准确度。

可是LRU算法是存在必定的问题

例如,这表示随着时间的推移,四个不一样的键访问。每一个“〜”字符为一秒钟,而“ |” 最后一行是当前时刻。

~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B〜|

~~~~~~~~~~ C ~~~~~~~~ C ~~~~~~~~~ C ~~~~~~ |

~~~~~ D ~~~~~~~~~ D ~~~~~~~ D ~~~~~~~~ D |


在上图中,按照LRU机制删除的话删除的顺序应该是C->A->B->D 其实这并非咱们想要的,由于B被访问的频率是最高的,而D被访问的频率比较低,因此咱们更想让B保留,把D删除,因此咱们接下来看另外一种策略  LFU

**LFU(leastFrequently used 最不常用)**

若是一个数据在最近一段时间内不多被访问到,那么能够认为在未来他被访问到的几率也很小。因此,当空间满时,最小频率访问的数据最早被淘汰

Redis使用redisObject中的24bit  lru字段来存储lfu字段, 这24bit被分为两部分:

1:高16位用来记录访问时间(单位为分钟) 

2:低8位用来记录访问频率,简称counter

    16 bits      8 bits
   
   +----------------+--------+
   
   Last decr time | LOG_C  |

可是counter 8bit很容易就溢出了,技巧是用一个逻辑计数器,给予几率的对数计数器,而不是一个普通的递增计数器
```
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;
}
```

对应的几率分布计算公式为
```
1.0/((counter - LFU_INIT_VAL)*server.lfu_log_factor+1);
```
其中LFU_INIT_VAL为5,其实简单说就是,越大的数,递增的几率越低
严格按照LFU算法,时间越久的key,counter越有可能越大,被剔除的可能性就越小。counter只增加不衰减就没法区分热点key。为了解决这个问题,redis提供了衰减因子server.lfu_decay_time,其单位为分钟,计算方法也很简单,若是一个key长时间没有访问那么他的计数器counter就要减小,减小的值由衰减因子来控制

> 关注个人技术公众号,每周都有优质技术文章推送。
微信扫一扫下方二维码便可关注:
![file](https://img2020.cnblogs.com/other/1094508/202010/1094508-20201018224534910-39047834.png)
相关文章
相关标签/搜索