redis内存限制管理---maxmemory配置详解

         作为内存数据库,为了防止redis占用过多的内存对其他的应用程序造成影响,redis的使用者可以在redis.conf文件中通过设置maxmemory选项对redis所能够使用的最大内存做限制,并通过maxmemory_policy对redis占用内存超过maxmemory之后的行为做定制。这篇文章,我们从redis源码的角度剖析一下redis的最大内存管理策略。

        redis源码中的内存管理策略都存在于evict.c文件中,其中最重要的一个函数就是freeMemoryIfNeeded。该函数用于在redis占用的内存超过maxmemory之后真正释放掉redis中的某些键值对,将redis占用的内存控制在合理的范围之内。

        redis在占用的内存超过指定的maxmemory之后,通过maxmemory_policy确定redis是否释放内存以及如何释放内存。redis提供了8种内存超过限制之后的响应措施,分别如下:

        1.volatile-lru(least recently used):最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉;

        2.volatile-lfu(least frequently used):最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉;

        3.volatile-ttl:从设置了过期时间的键中选择过期时间最早的键值对清除;

        4.volatile-random:从设置了过期时间的键中,随机选择键进行清除;

        5.allkeys-lru:最近最少使用算法,从所有的键中选择空转时间最长的键值对清除;

        6.allkeys-lfu:最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除;

        7.allkeys-random:所有的键中,随机选择键进行删除;

        8.noeviction:不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行;

        前缀为volatile-和allkeys-的区别在于二者选择要清除的键时的字典不同,volatile-前缀的策略代表从redisDb中的expire字典中选择键进行清除;allkeys-开头的策略代表从dict字典中选择键进行清除。这里简单介绍一下redis中struct redisDb的定义(更加详尽的解释会专门写一篇关于redisDb的博客)。在redis中每个struct redisDb包含5个指向dict的指针(dict*),这里我们重点关注两个dict*成员,分别是dict* dict和dict* expire。dict用于存放所有的键值对,无论是否设置了过期时间;expire只用于存放设置了过期时间的键值对的值对象。如果我们使用expire、expireat、pexpire、pexpireat、setex、psetex命令设置一个键的过期时间,那么将在dict *dict中创建一个sds字符串用于存放该键,dict和expire共享该键,以减少占用的内存;但是需要创建两个字符串对象分别存放该键关联的值(存放在dict中)和该键的过期时间(存放在expire中)。struct redisDb在server.h文件中的定义如下。

  typedef struct redisDb {
      dict *dict;                 /* The keyspace for this DB */
      dict *expires;              /* Timeout of keys with a timeout set */
      dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
      dict *ready_keys;           /* Blocked keys that received a PUSH */
      dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
      int id;                     /* Database ID */
      long long avg_ttl;          /* Average TTL, just for stats */
  } redisDb;

         在这篇文章中,我们重点关注的其中的三个变量dict *dict、dict *expire以及id。其中,id代表了该db在redisServer中的编号。redisServer一共存在dbnum个redisDb,server.dbnum是在redis启动的时候设置的,通过server.dbnum=CONFIG_DEFAULT_DBNUM(默认是16)进行初始化。

        上面简介了redis中内存超过限制时的几种策略,但是在redis的源码中是如何实现的?我们从redis-4.0.1源码实现的角度进行剖析。

        首先,redis在server.h文件中使用宏定义定义了上述几种内存管理策略。

  /* 0x0001 */
  #define MAXMEMORY_FLAG_LRU (1<<0)
  /* 0x0002 */
  #define MAXMEMORY_FLAG_LFU (1<<1)
  /* 0x0004 */
  #define MAXMEMORY_FLAG_ALLKEYS (1<<2)
  /* 0x0003 */
  #define MAXMEMORY_FLAG_NO_SHARED_INTEGERS \
      (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU)
  
  /* 0x0001 */
  #define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)
  /* 0x0102 */
  #define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)
  /* 0x0200 */
  #define MAXMEMORY_VOLATILE_TTL (2<<8)
  /* 0x0300 */
  #define MAXMEMORY_VOLATILE_RANDOM (3<<8)
  /* 0x0405 */
  #define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_ALLKEYS)
  /* 0x0506 */
  #define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU|MAXMEMORY_FLAG_ALLKEYS)
  /* 0x0604 */
  #define MAXMEMORY_ALLKEYS_RANDOM ((6<<8)|MAXMEMORY_FLAG_ALLKEYS)
  /* 0x0700 */
  #define MAXMEMORY_NO_EVICTION (7<<8)
  /* redis默认的超出内存限制的管理策略为noevction */
  #define CONFIG_DEFAULT_MAXMEMORY_POLICY MAXMEMORY_NO_EVICTION
         以上就是redis关于最大内存管理的8个宏定义,下面我们详细介绍LRU、LFU以及volatile-ttl算法的实现。在介绍redis中的lru和lfu算法之前,我们首先了解一下redis中保存键值的数据结构struct redisObject。
  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 decreas time). */
      int refcount;
      void *ptr;
  } robj;

         type代表了redis中的键对应的值是redis提供的五种对象之中的哪种类型;encoding是这种对象的底层实现方式,redis为每种对象至少提供了两种实现方式;lru,在使用lru相关的策略时,其24位保存的是以为单位的该对象上一次被访问的时间,如果使用的是lfu,那么高的16位以为单位保存着上一次访问的时间,低8位保存着按照某种规则实现的某段时间内的使用频次。以上元素都是使用“位域”实现,节省内存。refcount保存着该对象的引用计数,用于对象的共享和释放。ptr实际指向了该对象的底层实现。

        redis对于空间和时间效率的优化其实无处不在,在evict.c中当需要获取系统的时间时,redis采用了以下函数。

  unsigned int LRU_CLOCK(void) {
      unsigned int lruclock;
      if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
          atomicGet(server.lruclock,lruclock);
      } else {
          lruclock = getLRUClock();
      }
      return lruclock;
  }

        LRU_CLOCK(void)中1000/server.hz表示serverCron更新redisServer的lruclock的时间间隔,hz表示serverCron函数执行的频率,LRU_CLOCK_RESOLUTION表示系统配置的时钟精度,单位是ms。而getLRUClock的定义如下:

  /* 使用系统调用获取当前的时间 */
  unsigned int getLRUClock(void) {
      return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
  }

        getLRUClock(void)是直接调用系统的时钟函数mstime()得出结果,相比于直接原子的读取robj中的lruclock(atomicGet(server.lruclock, lruclock))数值的代价要高。按照getLRUClock()计算时间的方式,返回的时间精度为LRU_CLOCK_RESOLUTION毫秒。因此,当serverCron更新系统lruclock的时间间隔小于LRU_CLOCK_RESOLUTION的时候,系统通过serverCron更新lruclock的时间精度比直接调用getLRUClock得到的时间精度更高(间隔约小,时间精度越高),直接读取server.lruclock足以满足需求且减少了系统调用的时间开销。LRU_CLOCK_MAX为系统能够表示的时钟最大值,定义为#define LRU_CLOCK_MAX ((1 << LRU_BITS) - 1)。

        根据以上函数和robj中的lruclock字段就能够使用LRU相关的策略,计算该键的该键到目前的空转时长。redis中的计算方法定义为函数estimateObjectIdleTime(robj *o),返回的单位是ms。

  unsigned long long estimateObjectIdleTime(robj *o) {
      unsigned long long lruclock = LRU_CLOCK();
      /* if条件代表了lruclock和o->lru都在同一个范围之内的情况,表示没有发生回绕 */ 
      if (lruclock >= o->lru) { 
          return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
      /* 时钟的大小超过了lruclock能够表示的范围,发生了回绕,因此间隔的时间等于lruclock加上LRU_CLOCK_MAX减去o->lru。
       * linux内核中的jiffies回绕解决方案设计的更为精妙,可以参考《linux内核设计与实现》一书或博客的讲解。
       */
      } else {
          return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
                      LRU_CLOCK_RESOLUTION; 
      }               
  }   

        上面说了redis利用系统的时钟server.lruclock和robj的lru字段实现了LRU中估计空转时长的方法。LFU的计算也是利用robj中相同的字段。不过在LFU策略中,robj的lru字段被分为两个部分,高16位以“分”为单位记录上一次修改robj的时间,低8位按照某种特定的算法计算过去的某段时间间隔内该对象的使用频次。使用频次的增加和减少的计算方法分别定义为LFULogIncr(uint8_t counter)和LFUDecrAndReturn(robj *o)。下面分别给出定义。

        LFULogIncr(uint8_t counter)的定义如下:

  /* 概率性的增长趋势: 当前的counter值越大,那么在当前的值上增加的概率就越小;
   * 如果counter的值达到了255,那么直接返回255,不再增加任何的值
   */
  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),如图1所示(图片来源于https://yq.aliyun.com/articles/278922)。当达到计数能够表示的最大值255的时候,直接返回该计数。


图 1 计数增加概率分布

        当某个键的空转时长超过server设定的值server.lfu_decay_time的时候,那么需要减少该robj的使用计数,但是并不是直接将使用计数减1。而是采用了LogDecrAndReturn(robj* o)的算法,定义如下:

  unsigned long LFUDecrAndReturn(robj *o) {
      /* LFU策略下获取键的上一次更新的时间,单位为分钟 */
      unsigned long ldt = o->lru >> 8;
      /* 获取键的使用频次信息 */ 
      unsigned long counter = o->lru & 255;
      /* 如果距离上一次执行decrement的时间超过了lfu_decay_time的话,那么将使用频次减少,表示近期并没有使用到该键 */
      if (LFUTimeElapsed(ldt) >= server.lfu_decay_time && counter) {
          if (counter > LFU_INIT_VAL*2) {
              counter /= 2;
              if (counter < LFU_INIT_VAL*2) counter = LFU_INIT_VAL*2;
          } else {
              counter--;
          }   
          /* 更新键的LRU和使用频次信息 */
          o->lru = (LFUGetTimeInMinutes()<<8) | counter;
      }   
      return counter;
  }   

        其中LFUTimeElapsed(unsigned long ldt)的定义如下,用于计算键自从上一次被访问距离现在的时间,单位是"分"

 /* 距离上一次访问该数据对象已经过去了多少分钟 */
  unsigned long LFUTimeElapsed(unsigned long ldt) {
      unsigned long now = LFUGetTimeInMinutes();
      if (now >= ldt) return now-ldt;
      /* 如果now<ldt则有可能已经发生了回绕 */
      return 65535-ldt+now;
  }

        LFUGetTimeInMinutes(void)用于获取当前的分钟计数,redis为了避免每次使用时间的时候都进行系统调用,在redisServer中缓存了当前的秒的时间戳unixtime,以及毫秒的时间戳mstime。

  unsigned long LFUGetTimeInMinutes(void) {
      return (server.unixtime/60) & 65535;
  }

        这里有一个疑问,server中的lruclock和unixtime两个成员的差别是什么???我们知道redis在serverCron函数中更新server.lruclock的数值,合适更新unixtime和mstime呢?这个等看到相关的章节再来回答这个问题。

        以上,就是redis中关于LFU和LRU策略的量化方法,关于volatile-ttl策略的使用方式下面将会介绍。redis在需要释放内存的时候,可能不止需要释放一个键,释放内存的标准是释放之后使用内存的大小小于maxmemory的大小。因此,为了能够在每个redisDb中选择出合适数量的键进行释放,redis定义了server.maxmemory_sample参数,规定了每次在每个redisDb中最多选择出的待释放的键的最大个数,并且redis.conf中建议该变量的值为5。为了存储挑选出来的待释放的键,redis在evict.c中定义了如下变量。

  /* 存储待释放的键相关信息的存储空间能够容纳的键的个数*/
  #define EVPOOL_SIZE 16
  /* 可以放在cached中的最大的键的长度 */
  #define EVPOOL_CACHED_SDS_SIZE 255
  struct evictionPoolEntry {
      /* 空转的时长,无论是LRU、LFU还是TTL,最终都被转化成了idle进行衡量,详见evictionPoolPopulate的注释 */
      unsigned long long idle;    /* Object idle time (inverse frequency for LFU) */
      sds key;                    /* Key name. */
      sds cached;                 /* Cached SDS object for key name. */
      int dbid;                   /* Key DB number. */
  };
  /* 存储待释放的键的内存的头指针 */
  static struct evictionPoolEntry *EvictionPoolLRU;

        redis在每个dict中最多选择出server.maxmemory_sample个元素存放在以EvictionPoolLRU为开头的内存中。EvictionPoolLRU的初始化如下。

  void evictionPoolAlloc(void) {
      struct evictionPoolEntry *ep;
      int j;
  
      ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
      for (j = 0; j < EVPOOL_SIZE; j++) {
          ep[j].idle = 0;
          ep[j].key = NULL;
          /* 传入NULL表示只是分配空间,但是并不初始化为任何的数据 */
          ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
          ep[j].dbid = 0;
      }
      EvictionPoolLRU = ep;
  }

        在上面我们介绍了redis关于内存管理的LFU、LRU算法,并介绍了redis根据各种策略选择出的要清除的键在内存中存储的首地址。那么redis是如何使用各种键清楚策略将选择出的键放入到EvictionPoolLRU指向的内存中的呢?下面我们详细解释使用redis的各种键清除策略将选择出的键放入到EvictionPoolLRU指向的内存中的方法。针对该方法的理解都注释在函数中。

  void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
      int j, k, count;
      dictEntry *samples[server.maxmemory_samples];
  
       /* 从sampledict中最多选取server.maxmemory_samples个指向dictEntry的指针存放在放入到samples中,
        * 具体取出多少个由dictGetSomeKeys的返回值确定(返回值<= server.maxmemory_samples)
        */
      count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
      for (j = 0; j < count; j++) {
          unsigned long long idle;
          sds key;
          robj *o;
          dictEntry *de;
  
          de = samples[j];
          /* dictEntry中的key是sds类型的变量 */
          key = dictGetKey(de);
  
          /* 如果maxmemory_policy为MAXMEMORY_VALATILE_TTL,证明需要将距离过期时间最近的键清除掉;那么直接从redisDB->expire对应的字典中获取键对应
  的过期时间即可;那么此时不需要改变已经存在的de。如果maxmemory_policy != MAXMEMORY_VOLATILE_TTL且不是从redisDB->dict中获取键的值(使用redisDB->e  xpire中获取的键),那么需要从redisDB->dict中获取键对应的值的对象,才能够获取lru字段 ;*/
          if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
              if (sampledict != keydict) de = dictFind(keydict, key);
              o = dictGetVal(de);
          }
  
          if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
              idle = estimateObjectIdleTime(o);
          } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
              /* counter越大,代表近期使用的次数也就越多,当然也就对应于idle越小,不容易被清除掉 */
              idle = 255-LFUDecrAndReturn(o);
          } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
              /* maxmemory_policy = MAXMEMORY_VOLATILE_TTL的时候,那么只需要将de中的值取出即可,此时值对应的是键的过期时间。过期时间越小,那么>  对应的idle也就越大,就越应该被删除掉 */
               /* 之所以这里直接使用dictGetVal(de)是因为当存储的对象的值能够用long类型表示的时候,redis直接在(void)类型的ptr中存储long类型的数>  据 */
              idle = ULLONG_MAX - (long)dictGetVal(de);
          } else {
              serverPanic("Unknown eviction policy in evictionPoolPopulate()");
          }
  
          /* 根据EvictionPoolLRU指向的内存中已有元素的idle以及前面计算出索引为j元素的idle值,确定索引为j的元素是否应该插入到EvctionPoolLRU指向>  的连续内存中,以及如果需要插入进去,插入的位置在哪里。这里需要说明的一点是,EvictionPoolLRU中的元素全部按照元素对应的idle值按照从小到大的顺序
  进行排序。因此,idle越大,位置越靠后,当然也就最先被清除掉。根据这样的规律,插入过程中,如果K位置没有数据,那么K+1位置肯定没有数据 */
          k = 0;
          /* 寻找索引为j的元素在EvictionPoolLRU中合适的插入位置 */
          while (k < EVPOOL_SIZE &&
                 pool[k].key &&
                 pool[k].idle < idle) k++;
          /* pool中所有的元素都不为空,且其中元素的最小的idle大于等于idle */
          /* 需要说明的一点是,由于EvictionPoolLRU总是按照元素idle从小到大的顺序从前向后进行排列。因此,如果最后一个索引位置的key不为NULL,证明
  EvictionPoolLRU指向的内存已经满了。*/
          if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
              continue;
          } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
              /* k位置空闲,直接插入 */
          } else {
              /* EvictionPoolLRU中还有空闲空间,将k以及之后的数据全部向后移动一个单位 */
              if (pool[EVPOOL_SIZE-1].key == NULL) {
  
                  /* 实在是没有感觉到pool[k].cached目前有什么作用 ??? */
                  sds cached = pool[EVPOOL_SIZE-1].cached;
                  /* 证明需要在当前K的位置插入从选出的键,鉴于pool[EVPOOL_SIZE-1].key == NULL, 因此需要将目前的K~EVPOOL-2的元素移动到K+1 ~ EVPO  OL-1,共计EVPOOL-K-1个元素 */
                  memmove(pool+k+1,pool+k,
                      sizeof(pool[0])*(EVPOOL_SIZE-k-1));
                  pool[k].cached = cached;
              /* 没有空闲的空间,将小于当前idle的元素向前移动 */
              } else {
                  /* k最小为1,因为k ==0 && pool[EVPOOL_SIZE-1].key != NULL的情况已经被排除掉了 */
                  /* 没有空闲的空间,将小于当前idle的元素向前移动; k应该插入的位置为k-1 */
                  k--;
                  sds cached = pool[0].cached; /* Save SDS before overwriting. */
                  if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
                  /* 将1~k位置的元素移动到0 ~ k-1 */
                  memmove(pool,pool+1,sizeof(pool[0])*k);
                  pool[k].cached = cached;
              }
          }
  
          int klen = sdslen(key);
          if (klen > EVPOOL_CACHED_SDS_SIZE) {
              pool[k].key = sdsdup(key);
          } else {
              /* 一般情况下pook[k].key和pool[key].cached应该指向同一个位置 */
              memcpy(pool[k].cached,key,klen+1);
              sdssetlen(pool[k].cached,klen);
              pool[k].key = pool[k].cached;
          }
          pool[k].idle = idle;
          pool[k].dbid = dbid;
      }
  }

        evictionPoolPopulate()只是作为一个辅助函数,根据server的LRU、LFU以及volatile-ttl策略转换成的idle值将待删除的键相关的信息按照idle值从小到大的顺序从前向后存储在evctionPoolLRU中。但是,只是将单个redisDb中选择出的最多server.maxmemory_samples个元素存放到EvctionPoolLRU中。为了从整个server所有的redisDb中选择出较优的选项,redis使用freeMemoryIfNeeded()函数从所有的redisDb中选择键并进行释放。

        redis在统计占用的内存的时候没有将aof机制的缓冲区、aof重写缓冲区以及各个slave的输出缓冲区考虑在内,只是将估算出的实际数据占用的内存大小考虑在内。在释放redis使用的内存的时候又有如下辅助性的函数,size_t freeMemoryGetNotCountedMemory(void)用于估计上述三个缓冲区的大小,定义如下。

  size_t freeMemoryGetNotCountedMemory(void) {
      size_t overhead = 0;
      int slaves = listLength(server.slaves);
      
      if (slaves) {
          listIter li;
          listNode *ln;
  
          /* listNode中的void * value应该是指向client的指针 */
          listRewind(server.slaves,&li);
          while((ln = listNext(&li))) {
              client *slave = listNodeValue(ln);
              overhead += getClientOutputBufferMemoryUsage(slave);
          }
      }
      /* 获取aof缓冲区使用的大小和aof重写缓冲区的大小 */
      if (server.aof_state != AOF_OFF) {
          overhead += sdslen(server.aof_buf)+aofRewriteBufferSize();
      }
      return overhead;
  }

        下面,进入redis中最大内存管理策略的入口函数,freeMemoryIfNeeded(),完成释放一些键空间将redis使用的内存控制在server.maxmemory数值以下。

  int freeMemoryIfNeeded(void) {
      size_t mem_reported, mem_used, mem_tofree, mem_freed;
      mstime_t latency, eviction_latency;
      long long delta;
      int slaves = listLength(server.slaves);
  
      /* 如果所有的都在阻塞,并且阻塞时间未到,那么clientsArePaused返回为真 */
      /* 具体用途是啥 ??? */
      if (clientsArePaused()) return C_OK;
  
      /* 如果没有超过限制,那么没必要进行内存的清理工作 */
      mem_reported = zmalloc_used_memory();
      if (mem_reported <= server.maxmemory) return C_OK;
  
      /* server.maxmemory中对内存的限制并不将slaves output buffers和AOF buffer的消耗考虑在内 */
      mem_used = mem_reported;
      size_t overhead = freeMemoryGetNotCountedMemory();
      mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
  
      /* 将缓冲区使用的大小去掉之后,如果小于server.maxmemory的大小,那么直接返回,无需释放内存 */
      if (mem_used <= server.maxmemory) return C_OK;
  
      mem_tofree = mem_used - server.maxmemory;
      mem_freed = 0;
  
      /* 没有采取任何的内存限制策略,那么直接返回错误 */
      if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
          goto cant_free; /* We need to free memory, but policy forbids. */
  
      latencyStartMonitor(latency);
      while (mem_freed < mem_tofree) {
          int j, k, i, keys_freed = 0;
          static int next_db = 0;
          sds bestkey = NULL;
          int bestdbid;
          redisDb *db;
          dict *dict;
          dictEntry *de;
  
          /* 如果使用LRU、LFU或者是volatile-ttl策略清除内存空间,那么使用辅助函数以idle为标准收集需要释放的键 */
          if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
              server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
          {
              struct evictionPoolEntry *pool = EvictionPoolLRU;
  
              while(bestkey == NULL) {
                  unsigned long total_keys = 0, keys;
  
                  /* 我们不想只是针对server中的某一个db做过期键的清理工作,
                   * 因此,我们从每一个DB中获取过期键,然后放入到EvictionPoolLRU中 */
                  /* for循环从目前的server中的所有db中选出最佳的可以被淘汰的键进行淘汰 */
                  for (i = 0; i < server.dbnum; i++) {
                      db = server.db+i;
                      dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
                              db->dict : db->expires;
                      if ((keys = dictSize(dict)) != 0) {
                          evictionPoolPopulate(i, dict, db->dict, pool);
                          total_keys += keys;
                      }
                  }
                  /* 没有选择出任何的key,整个server中不存在任何的键 */
                  if (!total_keys) break; /* No keys to evict. */
  
                  /* 从EvctionPoolLRU中的最后一个元素(idle值最大)开始释放内存 */
                  for (k = EVPOOL_SIZE-1; k >= 0; k--) {
                      if (pool[k].key == NULL) continue;
                      bestdbid = pool[k].dbid;
  
                       /* 如果是allkeys-开头的删除策略,从dict字典中获取对象; 否则从expire字典中获取对象  */
                      if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
                          de = dictFind(server.db[pool[k].dbid].dict,
                              pool[k].key);
                      } else {
                          de = dictFind(server.db[pool[k].dbid].expires,
                              pool[k].key);
                      }
                      /* 已经获取了bestid和de,可以释放EvctionPoolLRU里面key对应的内存 */
                      if (pool[k].key != pool[k].cached)
                          sdsfree(pool[k].key);
                      pool[k].key = NULL;
                      pool[k].idle = 0;
  
                      if (de) {
                          bestkey = dictGetKey(de);
                          break;
                      } else {
                          /* Ghost... Iterate again. */
                      }
                  }
              }
          }
  
           /* 如果使用random相关的空间管理策略,则直接随机从dict或者expire中获取一个键进行删除,不需要辅助函数evctionPoolPopulate */
          else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                   server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
          {
              /* next_db为static int 类型,因此,每次调用获取db的id的时候总是能从不同的db中随机选取一个元素 */
              for (i = 0; i < server.dbnum; i++) {
                  j = (++next_db) % server.dbnum;
                  db = server.db+j;
                  dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
                          db->dict : db->expires;
                  if (dictSize(dict) != 0) {
                      de = dictGetRandomKey(dict);
                      bestkey = dictGetKey(de);
                      bestdbid = j;
                      break;
                  }
              }
          }
  
          /* 最终释放掉选择出的key对应的对象,这部分内容等阅读完redisDb、aof以及rdb相关的代码之后在进行说明 */
          if (bestkey) {
              db = server.db+bestdbid;
              /* 根据得到的sds类型的key创建redisObject对象 */
              robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
              propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
              /* We compute the amount of memory freed by db*Delete() alone.
               * It is possible that actually the memory needed to propagate
               * the DEL in AOF and replication link is greater than the one
               * we are freeing removing the key, but we can't account for
               * that otherwise we would never exit the loop.
               *
               * AOF and Output buffer memory will be freed eventually so
               * we only care about memory used by the key space. */
              delta = (long long) zmalloc_used_memory();
              latencyStartMonitor(eviction_latency);
              if (server.lazyfree_lazy_eviction)
                  dbAsyncDelete(db,keyobj);
              else
                  dbSyncDelete(db,keyobj);
              latencyEndMonitor(eviction_latency);
              latencyAddSampleIfNeeded("eviction-del",eviction_latency);
              latencyRemoveNestedEvent(latency,eviction_latency);
              delta -= (long long) zmalloc_used_memory();
              mem_freed += delta;
              server.stat_evictedkeys++;
              notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
                  keyobj, db->id);
              decrRefCount(keyobj);
              keys_freed++;
  
              /* When the memory to free starts to be big enough, we may
               * start spending so much time here that is impossible to
               * deliver data to the slaves fast enough, so we force the
               * transmission here inside the loop. */
              if (slaves) flushSlavesOutputBuffers();
  
              /* Normally our stop condition is the ability to release
               * a fixed, pre-computed amount of memory. However when we
               * are deleting objects in another thread, it's better to
               * check, from time to time, if we already reached our target
               * memory, since the "mem_freed" amount is computed only
               * across the dbAsyncDelete() call, while the thread can
               * release the memory all the time. */
              if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
                  overhead = freeMemoryGetNotCountedMemory();
                  mem_used = zmalloc_used_memory();
                  mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
                  if (mem_used <= server.maxmemory) {
                      mem_freed = mem_tofree;
                  }
              }
          }
  
          if (!keys_freed) {
              latencyEndMonitor(latency);
              latencyAddSampleIfNeeded("eviction-cycle",latency);
              goto cant_free; /* nothing to free... */
          }
      }
      latencyEndMonitor(latency);
      latencyAddSampleIfNeeded("eviction-cycle",latency);
      return C_OK;
  
  cant_free:
      /* We are here if we are not able to reclaim memory. There is only one
       * last thing we can try: check if the lazyfree thread has jobs in queue
       * and wait... */
      while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
          if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
              break;
          usleep(1000);
      }
      return C_ERR;
  }
        以上是关于个人对于redis内存管理策略的理解,其中打问号的地方是个人目前尚未解决的问题,欢迎吐槽。


猜你喜欢

转载自blog.csdn.net/gdj0001/article/details/80117797