Redis内存淘汰和过期删除策略原理分析

664次阅读  |  发布于1年以前

Redis是一个内存键值对数据库,所以对于内存的管理尤为重要。Redis内部对于内存的管理主要包含两个方向,过期删除策略和数据淘汰策略。
思考:

  • 什么是数据淘汰?
  • 数据过期和数据淘汰都是删除数据,两者有什么区别?
  • 实际使用场景是多样化的,如何选择合适的淘汰策略?

淘汰策略原理

所谓数据淘汰是指在Redis内存使用达到一定阈值的时候,执行某种策略释放内存空间,以便于接收新的数据。内存可使用空间由配置参数maxmemory决定(单位mb/GB)。故又叫"最大内存删除策略",也叫"缓存删除策略"。

maxmemory配置

# 客户端命令方式配置和查看内存大小
127.0.0.1:6379> config get maxmemory
"maxmemory"
"0"
127.0.0.1:6379> config set maxmemory 100mb
OK
127.0.0.1:6379> config get maxmemory
"maxmemory"
"104857600"

#通过redis.conf 配置文件配置
127.0.0.1:6379> info
# Server
#...
# 配置文件路径
config_file:/opt/homebrew/etc/redis.conf
#...

# 修改内存大小
> vim /opt/homebrew/etc/redis.conf
############################## MEMORY MANAGEMENT ################################

# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
#...
maxmemory 100mb
#...

注:若maxmemory=0则表示不做内存限制,但是对于windows系统来说,32位系统默认可使用空间是3G,因为整个系统内存是4G,需要留1G给系统运行。且淘汰策略会自动设置为noeviction,即不开启淘汰策略,当使用空间达到3G的时候,新的内存请求会报错。

淘汰策略分类

 # 命令行配置方式
127.0.0.1:6379> CONFIG GET maxmemory-policy
"maxmemory-policy"
"noeviction"
127.0.0.1:6379> CONFIG SET maxmemory-policy volatile-lru
OK
127.0.0.1:6379> CONFIG GET maxmemory-policy
"maxmemory-policy"
"volatile-lru"

#redis.conf文件配置方式
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# 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)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
# The default is:
# ...
maxmemory-policy noeviction

freeMemoryIfNeeded逻辑处理

int freeMemoryIfNeeded(void) {
  size_t mem_used, mem_tofree, mem_freed;
  int slaves = listLength(server.slaves);

  /* Remove the size of slaves output buffers and AOF buffer from the count of used memory.*/
  // 计算出 Redis 目前占用的内存总数,但有两个方面的内存不会计算在内:
  // 1)从服务器的输出缓冲区的内存
  // 2)AOF 缓冲区的内存
  mem_used = zmalloc_used_memory();
  if (slaves) {
    listIter li;
    listNode *ln;

    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
      redisClient *slave = listNodeValue(ln);
      unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
      if (obuf_bytes > mem_used)
        mem_used = 0;
      else
        mem_used -= obuf_bytes;
    }
  }
  if (server.aof_state != REDIS_AOF_OFF) {
    mem_used -= sdslen(server.aof_buf);
    mem_used -= aofRewriteBufferSize();
  }

  /* Check if we are over the memory limit. */
  // 如果目前使用的内存大小比设置的 maxmemory 要小,那么无须执行进一步操作
  if (mem_used <= server.maxmemory) return REDIS_OK;

  // 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回
  if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
    return REDIS_ERR; /* We need to free memory, but policy forbids. */

  /* Compute how much memory we need to free. */
  // 计算需要释放多少字节的内存
  mem_tofree = mem_used - server.maxmemory;

  // 初始化已释放内存的字节数为 0
  mem_freed = 0;

  // 根据 maxmemory 策略,
  // 遍历字典,释放内存并记录被释放内存的字节数
  while (mem_freed < mem_tofree) {
    int j, k, keys_freed = 0;

    // 遍历所有字典
    for (j = 0; j < server.dbnum; j++) {
      long bestval = 0; /* just to prevent warning */
      sds bestkey = NULL;
      dictEntry *de;
      redisDb *db = server.db+j;
      dict *dict;

      if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
        server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
      {
        // 如果策略是 allkeys-lru 或者 allkeys-random 
        // 那么淘汰的目标为所有数据库键
        dict = server.db[j].dict;
      } else {
        // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl 
        // 那么淘汰的目标为带过期时间的数据库键
        dict = server.db[j].expires;
      }

      // 跳过空字典
      if (dictSize(dict) == 0) continue;

      /* volatile-random and allkeys-random policy */
      // 如果使用的是随机策略,那么从目标字典中随机选出键
      if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
        server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
      {
        de = dictGetRandomKey(dict);
        bestkey = dictGetKey(de);
      }

      /* volatile-lru and allkeys-lru policy */
      // 如果使用的是 LRU 策略,
      // 那么从一集 sample 键中选出 IDLE 时间最长的那个键
      else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
        server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
      {
        struct evictionPoolEntry *pool = db->eviction_pool;

        while(bestkey == NULL) {
          evictionPoolPopulate(dict, db->dict, db->eviction_pool);
          /* Go backward from best to worst element to evict. */
          for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
            if (pool[k].key == NULL) continue;
            de = dictFind(dict,pool[k].key);

            /* Remove the entry from the pool. */
            sdsfree(pool[k].key);
            /* Shift all elements on its right to left. */
            memmove(pool+k,pool+k+1,
              sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
            /* Clear the element on the right which is empty since we shifted one position to the left.  */
            pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
            pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;

            /* If the key exists, is our pick. Otherwise it is a ghost and we need to try the next element. */
            if (de) {
              bestkey = dictGetKey(de);
              break;
            } else {
              /* Ghost... */
              continue;
            }
          }
        }
      }

      /* volatile-ttl */
      // 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键
      else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
        for (k = 0; k < server.maxmemory_samples; k++) {
          sds thiskey;
          long thisval;

          de = dictGetRandomKey(dict);
          thiskey = dictGetKey(de);
          thisval = (long) dictGetVal(de);

          /* Expire sooner (minor expire unix timestamp) is better candidate for deletion */
          if (bestkey == NULL || thisval < bestval) {
            bestkey = thiskey;
            bestval = thisval;
          }
        }
      }

      /* Finally remove the selected key. */
      // 删除被选中的键
      if (bestkey) {
        long long delta;

        robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
        propagateExpire(db,keyobj);
        // 计算删除键所释放的内存数量
        delta = (long long) zmalloc_used_memory();
        dbDelete(db,keyobj);
        delta -= (long long) zmalloc_used_memory();
        mem_freed += delta;

        // 对淘汰键的计数器增一
        server.stat_evictedkeys++;

        notifyKeyspaceEvent(REDIS_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();
      }
    }

    if (!keys_freed) return REDIS_ERR; /* nothing to free... */
  }

  return REDIS_OK;
}

8种淘汰策略

/* Redis maxmemory strategies */
 #define REDIS_MAXMEMORY_VOLATILE_LRU 0
 #define REDIS_MAXMEMORY_VOLATILE_TTL 1
 #define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
 #define REDIS_MAXMEMORY_ALLKEYS_LRU 3
 #define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
 #define REDIS_MAXMEMORY_NO_EVICTION 5
 #define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION

3.0版本提供6种策略:

4.0以上版本增加两种LFU策略:

淘汰策略的选择

# The counter decay time is the time, in minutes, that must elapse in order
# for the key counter to be divided by two (or decremented if it has a value
# less <= 10).
#
# The default value for the lfu-decay-time is 1. A special value of 0 means to
# decay the counter every time it happens to be scanned.
#
lfu-decay-time 1

Redis在实现淘汰策略时为了更合理的利用内存空间以及保证Redis的高性能,只是几近于算法的实现机制,其会从性能和可靠性层面做出一些平衡,故并不是完全可靠的。因此我们在实际使用过程中,建议都配置过期时间,主动删除那些不再使用的数据,以保证内存的高效使用。另外关于LRU和LFU算法,Redis内部在数据结构和实现机制上都做了一定程度的适应性改造

过期策略原理分析

众所周知,在Redis的实际使用过程中,为了让可贵的内存得到更高效的利用,我们提倡给每一个key配置合理的过期时间,以避免因内存不足,或因数据量过大而引发的请求响应延迟甚至是不可用等问题。
思考:

  • key的删除是实时的吗?
  • 是否存在并发和数据一致性问题?
  • 内存空间是有限的,除了过期策略,Redis还有什么其他保障?

过期Key删除原理

# expire: t秒后过期
expire key seconds
# pexpire: t毫秒后过期
pexpire key millseconds
# expireat: 到达具体的时间戳时过期,精确到秒
expireat key timestamp
# pexpireat: 到达具体的时间戳时过期,精确到毫秒
pexpire key millseconds

这四个命令看似有差异,但在RedisDb底层,最终都会转换成pexpireat指令。内部由db.c/expireGenericCommand函数实现,对外由上面四个指令调用

//expire命令
void expireCommand(redisClient *c) {
  expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
//expireat命令
void expireatCommand(redisClient *c) {
  expireGenericCommand(c,0,UNIT_SECONDS);
}
//pexpire命令
void pexpireCommand(redisClient *c) {
  expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
//pexpireat命令
void pexpireatCommand(redisClient *c) {
  expireGenericCommand(c,0,UNIT_MILLISECONDS);
}

/* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT
* and PEXPIREAT. Because the commad second argument may be relative or absolute
* the "basetime" argument is used to signal what the base time is (either 0
* for *AT variants of the command, or the current time for relative expires).
*/
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
  ...
  /* unix time in milliseconds when the key will expire. */
  long long when; 
  ...
  //如果是秒转换为毫秒
  if (unit == UNIT_SECONDS) when *= 1000;
  when += basetime;
  ...
}

删除方式优点缺点
定时删除能及时释放内存空间,不会产生滞留数据频繁生成和销毁定时器,非常损耗CPU性能,影响响应时间和指令吞吐量
定期删除固定的频率进行过期检查,对CPU交友好1.数据量比较大的情况下,会因为全局扫描而损耗CPU性能,且主线程的阻塞会导致其他请求响应延迟。2.未能及时释放内存空间。3.数据已过期,但定时器未执行时会导致数据不一致。
惰性删除节约CPU性能当某些数据长时间无请求访问时,会导致数据滞留,使内存无法释放,占用内存空间,甚至坑导致内存泄漏而引发服务不可用

由上述三种常用的删除方式对比结果可知,单独的使用任何一种方式都不能达到比较理想的结果,因此Redis的作者在设计过期删除策略的时候,结合了定期删除与惰性删除两种方式来完成。

删除方式优点缺点
Redis定期删除避免了全局扫描,每次随机抽取数据量较少,性能较稳定,执行频率可配置;避免了惰性删除低频数据长时间滞留的问题存在概率上某些数据一直没被抽取的情况,导致数据滞留
Redis惰性删除解决了定期删除可能导致的数据滞留现象,性能较高低频数据长时间无法释放

过期删除策略引起的脏读现象

在实际使用过程中,过期时间配置只是一种常规手段,当key的数量在短时间内突增,就有可能导致内存不够用。此时就需要依赖于Redis内部提供的淘汰策略来进一步的保证服务的可用性。

结语

到这里,我们可得出一个结论:Redis的高性能不仅仅体现在单线程上,还在于内存和数据管理的相辅相成上。除此之外,Redis的多样化数据结构和vm体系也为其高性能提供了更加有力的支撑,后续我们可以一起研究学习.

Copyright© 2013-2019

京ICP备2023019179号-2