内存淘汰策略

作为一个内存数据库,redis在内存空间不足的时候,为了保证命中率,就会选择一定的数据淘汰策略,这篇文章主要讲解常见的几种内存淘汰策略。

参数设置

redis可以通过maxmemory配置,来设置占用的最大内存,如果不设置或者设置为0,那么在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。
可以通过以下两种方式进行设置:

  • 配置文件redis.conf中设置 (推荐)
  • 使用命令设置,config set maxmemory (redis服务重启之后,设置的将失效)
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> config set maxmemory 100m
OK
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "100000000"
内存淘汰策略

如果设置了最大内存,那么当内存占用达到配置的上限时候,便会触发内存淘汰的策略,目前我使用的redis版本是redis-5.0.8,不同于之前的版本,redis5.0为我们提供了八个不同的内存置换策略,很早之前提供了6种。下面我们看一下这几种内存淘汰策略:

  • noeviction:默认策略,对于写请求直接返回错误
  • allkeys-lru:从所有key中使用LRU算法进行淘汰
  • volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
  • allkeys-random:从所有key中随机淘汰数据
  • volatile-random:从设置了过期时间的key中随机淘汰
  • volatile-ttl:从设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
  • allkeys-lfu:4.0新增,从所有key中使用LFU算法进行淘汰
  • volatile-lfu:4.0新增,从设置了过期时间的key中使用LFU算法进行淘汰

获取当前内存淘汰策略:

127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

通过配置文件设置淘汰策略(修改redis.conf文件):

maxmemory-policy allkeys-lru

通过命令修改淘汰策略:

127.0.0.1:6379> config set maxmemory-policy allkeys-lru

除了默认策略外,其他策略可以分为两部分:key的范围(allkeys,volatile),算法(ttl,random,lru,lfu)。
在volatile-lru、volatile-random、volatile-ttl、volatile-lfu这四种策略下,如果没有key可以被淘汰,则和noeviction一样直接返回错误。

redis服务器将所有的数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库。在初始化服务器时,程序会根据服务器状态的dbnum属性来决定创建 多少个数据库。

struct redisServer {
    redisDb *db;
    //此处省略部分代码
    int dbnum;             /* Total number of configured DBs */
}

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 */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

其中,dict变量保存了所有的键值对,expires变量保存了设置了过期时间的键值对。allkeys便是从dict中采样,volatile从expires中采样。

采样

为什么前面说allkeys是从dict中采样,volatile从expires中采样?因为在Redis的具体实现中(除了random算法之外),对样本的选择并不是全量,而是对全量数据进行随机采样。在3.0以前,采样方式比较简单,Redis通过随机采样法淘汰数据,每次随机出5个key(默认,通过maxmemory-samples参数配置),然后从里面按照算法淘汰掉对应的key。
在3.0以后,Redis对采样方式做了优化,新算法维护一个候选池,候选池中默认最多存放16个evictionPoolEntry对象:

struct evictionPoolEntry {
    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. */
};

候选池中的数据按照idle属性升序排列,而idle属性,不能以字面理解为key的空闲时间,而是根据不同算法,计算出来的不同的值,例如:

  • 当算法为LRU算法时,idle大的key,表示越久未被使用
  • 当算法为TTL算法时,idle大的key,表示越快过期
  • 当算法为LFU算法时,idle大的key,表示使用频率最低

其中,进入候选池的算法为:

  • 根据allkeys或者volatile,从dict或者expires中随机选取样本
  • 根据不同的算法,计算选取的样本idle值
  • 按照idle升序的规则,将样本插入到候选池对应的位置,若idle比候选池中第一个值的idle都小,则不插入池中

需要淘汰数据的时候,直接将候选池中的最后一个数据淘汰即可

候选池核心代码如下:

int freeMemoryIfNeeded(void) {
    // ...
    dict *dict;
    // TTL,LRU,LFU算法,使用候选池
    if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
        server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL){
        // // 根据allkeys或者volatile,选择不同的dict
        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;
        }
    }
    // random算法,不需要候选池
    else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
             server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM){
        // 根据allkeys或者volatile,选择不同的dict
        dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
            db->dict : db->expires;
        if (dictSize(dict) != 0) {
            // 获取随机样本
            de = dictGetRandomKey(dict);
            bestkey = dictGetKey(de);
            bestdbid = j;
            break;
        }
    }
	// ...
}

//候选池
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    int j, k, count;
    dictEntry *samples[server.maxmemory_samples];

    // 随机获取部分样本
    // sampledict为根据allkeys或者volatile选择的rediDb.dict或者rediDb.expires
    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    for (j = 0; j < count; j++) {
        unsigned long long idle;
        // 根据不同的淘汰算法,计算样本的idle值
        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
            // LRU算法,则计算样本的空闲时间
            idle = estimateObjectIdleTime(o);
        } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
            // LFU算法,则计算样本的使用频率
            idle = 255-LFUDecrAndReturn(o);
        } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
            // TTL算法,额计算样本的过期时间
            idle = ULLONG_MAX - (long)dictGetVal(de);
        } else {
            serverPanic("Unknown eviction policy in evictionPoolPopulate()");
        }

        k = 0;
        // 查询插入位置
        while (k < EVPOOL_SIZE && pool[k].key && pool[k].idle < idle) 
            k++;
        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
            // 样本比第一个元素的idle都要小,则不满足插入条件
            continue;
        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
            // 满足插入条件,则不做任何处理
        } else {
            // 在中间位置插入
            if (pool[EVPOOL_SIZE-1].key == NULL) {
                // 候选池不够16个,而追加到末尾
                // ...
            } else {
                // 插入到中间位置,则空出中间位置,数据右移
                // ...
            }
        }
        // 执行插入 ...
    }
}
结束语

本篇主要是介绍redis的内存淘汰策略,其中涉及了很多的算法,例如,random,ttl,lru,lfu,具体的算法在这里我们就不继续探讨下去了,后序可能会增加这类算法的分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丿微风乍起

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值