• Redis中的内存管理策略
  • 发布于 2个月前
  • 168 热度
    0 评论
  • 江起京
  • 0 粉丝 16 篇博客
  •   

Redis在服务端分别为不同的db index维护一个dict,这个dict称为key space键空间。每一个RedisClient只能属于一个db index,在Redis服务端会维护每一个链接的RedisClient。

typedef struct redisClient {
    uint64_t id;
    int fd;
    redisDb *db;
} redisClient;

在服务端每一个Redis客户端都会有一个指向redisDb的指针。

typedef struct redisDb {
    dict *dict;
    dict *expires;
    dict *blocking_keys;
    dict *ready_keys;
    dict *watched_keys;
    struct evictionPoolEntry *eviction_pool;
    int id;
    long long avg_ttl;
} redisDb;

key space键空间就是这里的redisDb->dict。redisDb->expires是维护所有键空间的每一个key的过期时间。


1、键过期时间、生存时间

对于一个key我们可以设置它多少秒、毫秒之后过期,也可以设置它在某个具体的时间点过期,后者是一个时间戳。例如:

EXPIRE命令可以设置某个key多少秒之后过期;
PEXPIRE命令可以设置某个key多少毫秒之后过期;
EXPIREAT命令可以设置某个key在多少秒时间戳之后过期;
PEXPIREAT命令可以设置某个key在多少毫秒时间戳之后过期;
PERSIST命令可以移除键的过期时间。

其实上述命令最终都会被转换成对PEXPIREAT命令。在redisDb->expires指向的key字典中维护着一个到期的毫秒时间戳。

TTL、PTTL可以通过这两个命令查看某个key的过期秒、毫秒数。

Redis内部有一个事件循环,这个事件循环会检查键的过期时间是否小于当前时间,如果小于则会删除这个键。


2、过期键删除策略

在使用Redis的时候我们最关心的就是键是如何被删除的,如何高效准时地删除某个键。其实Redis提供了两个方案来完成这件事情:惰性删除、定期删除双重删除策略。

惰性删除:当我们访问某个key的时候,Redis会检查它是否过期。

robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;

    expireIfNeeded(db,key);
    val = lookupKey(db,key);
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;
    return val;
}

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;

    if (when < 0) return 0; /* No expire for this key */

    if (server.loading) return 0;

    now = server.lua_caller ? server.lua_time_start : mstime();
    if (server.masterhost != NULL) return now > when;

    /* Return when this key has not expired */
    if (now <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);
    return dbDelete(db,key);
}

定期删除:Redis通过事件循环,周期性地执行key的过期删除动作。

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    /* Handle background operations on Redis databases. */
    databasesCron();
}

void databasesCron(void) {
    /* Expire keys by random sampling. Not required for slaves
     * as master will synthesize DELs for us. */
    if (server.active_expire_enabled && server.masterhost == NULL)
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
}

要注意的是:

惰性删除是每次只要有读取、写入都会触发惰性删除代码;
定期删除是由Redis EventLoop来触发的。Redis内部很多维护性工作都是基于EventLoop。


3、AOF、RDB处理过期键策略

既然键会随时存在过期问题,那么涉及到持久化Redis是如何帮我们处理的?

当Redis使用RDB方式持久化时,每次持久化的时候就会检查这些即将被持久化的key是否已经过期,如果过期将直接忽略,持久化那些没有过期的键。

当Redis作为master主服务器启动的时候,载入rdb持久化键时也会检查这些键是否过期,将忽略过期的键,只载入没过期的键。

当Redis使用AOF方式持久化时,每次遇到过期的key Redis会追加一条DEL命令到AOF文件,也就是说只要我们顺序载入执行AOF命令文件就会删除过期的键。

如果Redis作为从服务器启动的话,它一旦与master主服务器建立链接就会清空所有数据进行完整同步。当然新版本的Redis支持SYCN2的半同步,如果是已经建立了master/slave主从同步之后,主服务器会发送DEL命令给所有从服务器执行删除操作。


4、Redis LRU算法

在使用Redis的时候我们会设置maxmemory选项,64位的默认是0不限制。线上的服务器必须要设置的,要不然很有可能导致Redis宿主服务器直接内存耗尽,最后链接都上不去。

所以基本要设置两个配置:

maxmemory最大内存阈值;
maxmemory-policy到达阈值的执行策略。

可以通过CONFIG GET maxmemory/maxmemory-policy分别查看这两个配置值,也可以通过CONFIG SET去分别配置。

maxmemory-policy有一组配置,可以用在很多场景下:

.noeviction:客户端尝试执行会让更多内存被使用的命令直接报错;
.allkeys-lru:在所有key里执行lru算法;
.volatile-lru:在所有已经过期的key里执行lru算法;
.allkeys-random:在所有key里随机回收;
.volatile-random:在已经过期的key里随机回收;
.volatile-ttl:回收已经过期的key,并且优先回收存活时间(TTL)较短的键。

关于cache的命中率可以通过info命令查看键空间的命中率和未命中率。

# Stats
keyspace_hits:33
keyspace_misses:5

maxmemory在到达阈值的时候会采用一定的策略去释放内存,这些策略我们可以根据自己的业务场景来选择,默认是noeviction 。

Redis LRU算法有一个取样的优化机制,可以通过一定的取样因子来加强回收的key的准确度。CONFIG GET maxmemory-samples查看取样配置,具体可以参考更加详细的文章。

用户评论