• Redis内存回收


    Redis本质上一个存储系统。所有的存储系统在数据量过大的情况下都会面临存储瓶颈,包括 MySQL, RabbitMQ等等。

    这里我们解决要两个问题:
    首先,作为一个内存的KV系统,Redis服务肯定不是无限制地使用内存,应该设置一个上限(max_memory)。
    第二个,数据应该有过期属性, 这样就能清除不再使用的key。

    网上有一个面试题就是:Redis内存满了怎么办?

    我们先看一下key过期怎么处理,再看内存达到上限怎么处理。

    内存回收

    过期策略

    要实现key过期,我们有几种思路。

    立即过期(主动淘汰)

    每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

    惰性过期(被动淘汰)

    只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
    例如所有的查询都会调用expirelfNeeded判断是否过期:

    db.c 1299 行

    expireIfNeeded(redisDb *db, robj *key)
    

    第二种情况,每次写入key时,发现内存不够,调用activeExpireCycle释放一部分内存。

    expire.c 123行

    activeExpireCycle(int type)
    

    定期过期

    每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

    server.h 661 行

    typedef struct redisDb {
      diet *dict;       /* 所有的键值对 */
      diet *expires;     /* 设置 了过期时间的键值对 */ 
      diet *blocking_keys;
      diet *ready_keys;
      diet *watched_keys; 
      int id; 
      long long avg_ttl;
      unsigned long expires_cursor; 
      list *defrag_later; 
    } redisDb;
    

    总结:Redis中同时使用了惰性过期和定期过期两种过期策略,并不是实时地清除过期的key。如果所有的key都没有设置过期属性,Redis内存满了怎么办?

    淘汰策略

    Redis的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。

    最大内存设置

    # maxmemory <bytes>
    

    如果不设置maxmemory或者设置为0, 32位系统最多使用3GB内存,64位系统不限制内存。

    动态修改(先get一下):

    redis> config set maxmemory 2GB
    

    到达最大内存以后怎么办?

    淘汰策略

    https://redis.io/topics/lru-cache

    redis.conf

    maxmemory-policy noeviction

    # volatile-lru -> Evict using approximated LRU, only keys with an expire set.
    # allkeys-lru -> Evict any key using approximated LRU.
    # volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
    # allkeys-lfu -> Evict any key using approximated LFU.
    # volatile-random -> Remove a random key having an expire set.
    # allkeys-random -> Remove a random key, any key.
    # volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
    

    先从后缀的算法名来看:

    LRU, Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
    LFU, Least Frequently Used,最不常用,按照使用频率删除,4.0版本新增。
    random,随机删除。
    从前缀针对的对象来分:volatile是针对设置了ttl的key, allkeys是针对所有key。

    策略 含义
    volatile-lru 根据LRU算法删除设置超时属性(expire)的键,直到腾岀足够内存为止。如果没有可删除的键对象,回退到noeviction策略。
    allkeys-lru 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
    volatile-lfu 在带有过期时间的键中选择最不常用的。
    allkeys-lfu 在所有的键中选择最不常用的,不管数据有没有设置超时属性。
    volatile-random 在带有过期时间的键中随机选择。
    allkeys-random 随机删除所有键,直到腾出足够内存为止
    volatile-ttl 根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
    noeviction 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error ) OOM command not allowed when used memory,此时 Redis 只响应读操作。

    如果没有设置ttl或者没有符合前提条件的key被淘汰,那么volatile-lru、 volatile-random、volatile-ttl 相当于 noeviction (不做内存回收)。

    动态修改淘汰策略(先get一下):

    redis> config set maxmemoiy-policy volatile-lru
    

    建议使用volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key。

    LRU淘汰原理

    LRU是一个很常见的算法,比如InnoDB的Buffer Pool也用到了 LRU。
    传统的LRU:通过链表+HashMap实现,设置链表长度,如果新增或者被访问,就移动到头节点。超过链表长度,末尾的节点被删除。

    问题:如果基于传统LRU算法实现Redis LRU会有什么问题?

    需要额外的数据结构存储,消耗内存。Redis LRU对传统的LRU算法进行了改良,通过随机采样来调整算法的精度。

    如果淘汰策略是LRU,则根据配置的采样值maxmemory_samples (默认是5个), 随机从数据库中选择m个key,淘汰其中热度最低的key对应的缓存数据。所以采样参数m配置的数值越大,就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计 算,执行效率降低。

    问题:如何找出热度最低的数据?

    Redis中所有对象结构都有一个Iru字段,且使用了 unsigned的低24位,这个字段 用来记录对象的热度。对象被创建时会记录Iru值。在被访问的时候也会更新Iru的值。 但并不是获取系统当前的时间戳,而是设置为全局变量server.lruclock的值

    源码:server.h 622 行

    typedef struct redisObject (
      unsigned type:4;       /* 对象的类型,包括:OBJ_STRING, OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
      unsigned encoding:4;      /* 具体的数据结构 */
      unsigned lru:LRU_BITS;       /* 24位,对象最后一次被命令程序访问的时间,与内存回收有关*/
                                             /* LRU time (relative to global lru_clock) or
                                              * LFU data (least significant 8 bits frequency
                                              * and most significant 16 bits access time). */
     	
      int refcount;                  /*引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了 */
      void *ptr;                      /*指向对象实际的数据结构*/
    } robj;
    

    server.lruclock的值怎么来的?

    Redis中有个定时处理的函数serverCron ,默认每100毫秒调用函数 updateCachedTime更新一次全局变量的server.lruclock的值,它记录的是当前unix 时间戳。

    源码:server.c 1756 行

    void updateCachedTime(int update_daylight_info) {
      server.ustime = ustime();
      server.mstime = server.ustime / 1000;
      server.unixtime = seiver.mstime / 1000;
      /* To get information about daylight saving time, we need to call
        * localtime r and cache the result. However calling localtime_r in this
        * context is safe since we will never fbrk() while here, in the main
        * thread. The logging fimction will call a thread safe version of
        * localtime that has no locks. */  
      if (update_daylight_info) {
        struct tm tm;
        time_t ut = seiver.unixtime;
        localtime_r(&ut,&tm);
        server. daylight_active = tm.tm_isdst;
      } 
    }
    

    问题:为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?

    这样函数査询key调用lookupKey中更新数据的Iru热度值时,就不用每次调用系统函数time,可以提高执行效率。OK,当对象里面已经有了 LRU字段的值,就可以评估对象的热度了。

    /* Given an object returns the min number of milliseconds the object was never
    * requested, using an approximated LRU algorithm. */
    unsigned long long estimateObjectIdleTime(robj *o) {
      unsigned long long Iruclock = LRU_CLOCK();
      if (Iruclock >= o->lru) (
        return (Iruclock - o->lru) * LRU_CLOCK_RESOLUTION;
      } else (
        return (Iruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION;
      }
    }
    

    函数estimateObjectldleTime评估指定对象的Iru热度,方法就是对象的Iru值和全局的server.lruclock的差值越大(越久没有得到更新),该对象热度越低。
    server.lruclock只有24位,按秒为单位来表示才能存储194天。当超过24bit能表示的最大时间的时候,它会从头开始计算。
    在这种情况下,可能会出现对象的Iru大于server.lruclock的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。

    为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。

    而Redis LRU算法在sample为10的情况下,已经能接近传统LRU算法了。

    https://redis.io/topics/lru-cache

    问题:除了消耗资源之外,传统LRU还有什么问题?

    如图,假设A在10秒内被访问了5次,而B在10秒内被访问了3次。因为B最后一次被访问的时间比A要晚,在同等的情况下,A反而先被回收。

    问题:要实现基于访问频率的淘汰机制,怎么做?

    LFU

    server.h

    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;
    

    当这24 bits用作LFU时,其被分为两部分:
    高16位用来记录访问时间(单位为分钟,Idt, last decrement time)
    低8位用来记录访问频率,简称counter (logc, logistic counter) counter是用基于概率的对数计数器实现的,8位可以表示百万次的访问频率。 对象被读写的时候,lfu的值会被更新。

    db.c 55 行 ---lookupKey

    void updateLFU(robj *val) {
      unsigned long counter = LFUDecrAndReturn(val);
      counter = LFULogIncr(counter);
      val->lru = (LFUGetTimelnMinutes()«8) | counter;
    }
    

    当然,这里并不是访问一次,计数就加1。增长的速率由一个参数决定,Ifu-log-factor越大,counter增长的越慢
    redis.conf配置文件:

    # lfu-log-factor 10
    

    注意一下,这个算法是LRU,如果一段时间热点高,就一直保持这个热度,肯定也是不行的,体现不了整体频率。所以,没有被访问的时候,计数器还要递减。
    没有被访问的时候,计数器怎么递减呢?
    减少的值由衰减因子Ifu-decay-time (分钟)来控制,如果值是1的话,N分钟没有访问,计数器就要减少N。Ifu-decay-time越大,衰减越慢。

    redis.conf配置文件

    # lfu-decay-time 1
    
  • 相关阅读:
    【CF103D】Time to Raid Cowavans-分块+离线处理
    【BZOJ3992】序列统计(SDOI2015)-NTT+循环卷积+快速幂
    【BZOJ3527】力(ZJOI2014)-FFT
    【HDU4609】3-idiots-FFT+生成函数
    【LuoguP3803】多项式乘法-FFT/NTT模板题(附带FFT/NTT简单介绍)
    网络流24题解题总结(更新中)
    【BZOJ3531】旅行(SDOI2014)-树链剖分+动态加点线段树
    [Noip2012]借教室
    bzoj3394:[Usaco2009 Jan]Best Spot 最佳牧场
    [NOIP2014]无线网站发射器选址
  • 原文地址:https://www.cnblogs.com/snail-gao/p/14431754.html
Copyright © 2020-2023  润新知