Redis 源码分析客户端数据结构(serverDb)

2,063 阅读5分钟

「这是我参与2022首次更文挑战的第42天,活动详情查看:2022首次更文挑战」。

数据库存储在 redisDb 结构中,而服务端 redisServer 结构中保存着 redisDb 对象和个数,个数可以在配置文件中进行更新。

image-20220227203958793.png

数据结构

typedef struct redisDb {
    // 保存 k,v 数据
    dict *dict;                 /* The keyspace for this DB */
    // 保存 k, exprie 时间
    dict *expires;              /* Timeout of keys with a timeout set */
    // 阻塞的 key 
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    // 
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    // 监控的 key
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

而 redisServer 中都包含了 redisDb 数据结构。表示当前使用的是那个 db。

struct redisServer {
    /* General */
    pid_t pid;                  /* Main process pid. */
    pthread_t main_thread_id;         /* Main thread id */
    char *configfile;           /* Absolute config file path, or NULL */
    char *executable;           /* Absolute executable file path. */
    char **exec_argv;           /* Executable argv vector (copy). */
    int dynamic_hz;             /* Change hz value depending on # of clients. */
    // ...
};

数据库切换

数据库切换是使用 select 命令来执行,实际上第 n 个 db 指向客户端的 db 对象 c -> db = &server.db[id]

int selectDb(client *c, int id) {
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}

键空间

由前文可以知道 redis 可以有多个数据库,而不同数据库之间是不相互影响的,如何做到的呢?键空间可以理解成 C++ 里面的命名空间,用来隔离,数据存在 redisDb 中的dict 对象,因此对于键的操作,基本都是基于键空间来操作的。

本质其实就是每个 redisDb 中的 dict 对象。很好理解,因为选择了不同的 db,肯定下面的键也不一样。

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    signalKeyAsReady(db, key, val->type);
    if (server.cluster_enabled) slotToKeyAdd(key->ptr);
}

过期键

  1. 在 redis 中过期键保存在 redisD中的 expres 变量里中, expires 是 dict 指针类型
  2. 存储方式
    • key 保存的是数据库的键对象
    • value 保存的是数据对象的过期时间,长整型 Unix 时间。
void setExpire(client *c, redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr);
    serverAssertWithInfo(NULL,key,kde != NULL);
    dictSetSignedIntegerVal(de,when);
    de = dictAddOrFind(db->expires,dictGetKey(kde));

    int writable_slave = server.masterhost && server.repl_slave_ro == 0;
    if (c && writable_slave && !(c->flags & CLIENT_MASTER))
        rememberSlaveKeyWithExpire(db,key);
}
  1. 设置时间

相对方式:expire

绝对方式:expireat

如 expire 命令:

setExpire(c, c->db, key, when)

---> kde = dictFind(db->dict,key->ptr);

---> de = dictAddOrFind(db->expires,dictGetKey(kde));

  1. 删除过期时间

persist

  1. 查看过期时间:ttl

lookupKeyReadWithFlags --> getExpire --> ttl = expire-mstime();

  1. 键过期策略

    • 定期删除,每间隔一段时间,程序对数据进行一次勘查,删除里面的过期键。对内存友好,尽可能的删掉一些过期的键,但是对 CPU 不友好,而且需要设置好定期时间以及每次删除数量

    • 定时删除,在设计过期键的时候,建立定时器,让定时器在键过期时间达到时,立即删除对键的操作,可以看作一种时删除的一种,但是该方案坑你创建较多的定时器,而且 redis 定时器采用的是链表,查找某个 key 时间负载度为 O(n)

    • 惰性删除:每次在获取键的时候,来处理过期键,确定是对内存不友好

    • 触发清理策略:当设置了 maxmemory , 且超过过期时间。

    redis 中采用的是定期删除和惰性删除两种

  2. 定期删除分为两种模式,分别在不同的场景下使用。因此结束判断不一样。

在定时任务 serverCorn 中的 databaseCron 中,为 ACTIVE_EXPIRE_CYCLF_SLOW, 这就意味着我们可以花费更多的时间来处理。而在十佳循环之前的 beforeSleep 函数中则为 ACTIVE_EXPIRE_CYCLE_FAST,因为不能影响处理时间,还做一个优化

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exit
         * for time limit, unless the percentage of estimated stale keys is
         * too high. Also never repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit &&
            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
            return;

        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
            return;

        last_fast_cycle = start;
    }
  1. REDIS_EXPIRELOOKUPS_TIME_PERC 是单位时间内分配给 activeExpireCycle 函数执行的 CPU 比例, 默认为。25

  2. 每次循环最多 16 个库。

  3. 每个库要求找到过期键达到 5 个就行(注意的是,因为那个库随机选取一个key 的,所以数量不能太少,太少随机效果不好)。

  4. 每个库里面,每次随机选取 20 个 key 。 检查是否过期键,过期就记数一次。每间隔 16 次检查一下时间是否超过限制,如果超过需要推出玄幻。不在继续查找。

对于过期键 RDB/AOF/主从的影响,后期讲述。

通知和事件

数据库通知主要是利用 redis 中支持发布订阅模式,让客户端通过订阅给顶的频道或者模式(保存在 client 和 server 的 pubsub_channels 和 pubsub_patterns, 但保存的内容不一样)来获取数据库中的键的变化。主要是分为键空间通知和事件通知两类,要开启该功能需要在配置文件中设置参数/动态设置 notify-keyspace-events

键空间通知

keyspace@ 某个键执行了什么命令

事件通知

keyevent@ 某个事件被哪些命令执行

启用事件通知有两种方式:

  1. 配置文设置:格式: notify-keyspace-events Elg. 如果要配置 K 和 E 必须要开启一个。
  2. 命令设置
config set notify-keyspace-events Elg

image-20220228220248484.png

image-20220228220335486.png

3、查看发布订阅的 key

PSUBSCRICE __keyevent@*__:expired

4、也可以使用 tcpdump 抓包来查看