redis源码注释七:数据库db.c

0. 本文要点

主要讲和Redis 数据库的构造和实现。
1)数据库如何存储对象?redisDb数据库结构体
2)数据库中键的操作
3)键的过期时间
4)过期键的处理
5)过期键与AOF和RDB

1. 数据库结构体

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)*/ //键为阻塞客户端的键,value为因键阻塞的所有客户端(链表串起来)
    dict *ready_keys;           /* Blocked keys that received a PUSH */ //被阻塞的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */ //被WATCH命令监控的键,用于事务
    int id;                     /* Database ID */ //数据库的id,每个server有多个数据库,数据库之间通过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;

下文将详细讨论iddictexpires 三个属性。
先讲id。
redis在启动服务器的时候,会创建CONFIG_DEFAULT_DBNUM个数据库,CONFIG_DEFAULT_DBNUM的值在server.h中定义,为16,相当于一个包含16redisDb的数组,id就是某个redisDb在数组中的下标。

创建数据库的代码如下(server.c):

//initServerConfig函数
server.dbnum = CONFIG_DEFAULT_DBNUM;
//initServer函数
server.db = zmalloc(sizeof(redisDb)*server.dbnum);

不同的客户端可能连接到不同的数据库上,内部程序如AOF根据id就可以知道使用的是哪个数据库,而且切换数据库方便。

2. 数据库中的键

redisDb使用一个dict来保存和对应的value,其中:
键是字符串对象;
值是五种对象(字符串,列表,哈希表,集合,有序集合)之一。
 》

图片截取自《redis设计与实现》

键的操作基本上就是哈希表的操作。

2.1 添加键值对

void dbAdd(redisDb *db, robj *key, robj *val) { //往数据库中加入键值对
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);//将key value加入字典

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET)
        signalKeyAsReady(db, key);// 如果值对象是列表或有序集合类型,有阻塞的命令,因此将key加入ready_keys字典中
    if (server.cluster_enabled) slotToKeyAdd(key);
}

2.2 复写

void dbOverwrite(redisDb *db, robj *key, robj *val) { //复写key对应的值
    dictEntry *de = dictFind(db->dict,key->ptr);

    serverAssertWithInfo(NULL,key,de != NULL);
    dictEntry auxentry = *de;
    robj *old = dictGetVal(de);
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        val->lru = old->lru;
    }
    dictSetVal(db->dict, de, val);//设置新值

    if (server.lazyfree_lazy_server_del) {
        freeObjAsync(old);
        dictSetVal(db->dict, &auxentry, NULL);
    }

    dictFreeVal(db->dict, &auxentry);
}

2.3 查找

robj *lookupKey(redisDb *db, robj *key, int flags) { //查找key,一般被lookupKeyWrite lookupKeyReadWithFlags lookupKeyRead调用
    dictEntry *de = dictFind(db->dict,key->ptr);//在字典中查找key
    if (de) { //如果找到
        robj *val = dictGetVal(de);//获取值

        /* Update the access time for the ageing algorithm.
         * Don't do it if we have a saving child, as this will trigger
         * a copy on write madness. */
        if (server.rdb_child_pid == -1 &&
            server.aof_child_pid == -1 &&
            !(flags & LOOKUP_NOTOUCH))
        { //更新键的时间
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                updateLFU(val);
            } else {
                val->lru = LRU_CLOCK();
            }
        }
        return val;//找到返回相应的值
    } else {
        return NULL;//找不到返回空
    }
}

/* Lookup a key for read operations, or return NULL if the key is not found
 * in the specified DB.
 *
 * As a side effect of calling this function:
 * 1. A key gets expired if it reached it's TTL. //如果键到达TTL则键设置为过期
 * 2. The key last access time is updated. //更新键的最近一次获取时间
 * 3. The global keys hits/misses stats are updated (reported in INFO).//全局键的命中/未命中状态会更新
 *
 * This API should not be used when we write to the key after obtaining
 * the object linked to the key, but only for read only operations.
 *
 * Flags change the behavior of this command:
 *
 *  LOOKUP_NONE (or zero): no special flags are passed.
 *  LOOKUP_NOTOUCH: don't alter the last access time of the key.
 *
 * Note: this function also returns NULL if the key is logically expired       //如果一个键存在但是过期了,函数会返回空
 * but still existing, in case this is a slave, since this API is called only
 * for read operations. Even if the key expiry is master-driven, we can
 * correctly report a key is expired on slaves even if the master is lagging
 * expiring our key via DELs in the replication link. */
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

    if (expireIfNeeded(db,key) == 1) {
        /* Key expired. If we are in the context of a master, expireIfNeeded()
         * returns 0 only when the key does not exist at all, so it's safe
         * to return NULL ASAP. */
        if (server.masterhost == NULL) {
            server.stat_keyspace_misses++;
            return NULL;
        }

        /* However if we are in the context of a slave, expireIfNeeded() will
         * not really try to expire the key, it only returns information
         * about the "logical" status of the key: key expiring is up to the
         * master in order to have a consistent view of master's data set.
         * //从节点中调用expireIfNeeded不会将键设置为过期,以保持和主节点的一致性,设置键的过期由主节点完成
         * However, if the command caller is not the master, and as additional
         * safety measure, the command invoked is a read-only command, we can
         * safely return NULL here, and provide a more consistent behavior
         * to clients accessign expired values in a read-only fashion, that
         * will say the key as non existing.
         *
         * Notably this covers GETs when slaves are used to scale reads. */
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            server.stat_keyspace_misses++;
            return NULL;
        }
    }
    val = lookupKey(db,key,flags);
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;
    return val;
}

3. 键的过期时间

3.1 设置键的过期时间

可以通过EXPIREPEXPIREEXPIREATPEXPIREAT 四个命令设置键的过期时间.

命令含义
EXPIRE以秒为单位设置键的生存时间
PEXPIRE以毫秒为单位设置键的生存时间
EXPIREAT以秒为单位,设置键的过期UNIX 时间戳
PEXPIREAT以毫秒为单位,设置键的过期UNIX 时间戳

虽然设置键的过期时间(生存时间)的命令有四个,但是最终存储过期信息的时候,都是存储的"以毫秒为单位的过期UNIX 时间戳".键的过期时间存储在字典expires中.

3.2 expires

这是一个字典,字典的key是指向键的指针,字典的值是对应键的过期时间,以long long 类型表示。

在这里插入图片描述
expires 字典的值只保存“以毫秒为单位的过期UNIX 时间戳” ,通过进行转换,所有命令的效果最后都和PEXPIREAT命令的效果一样。

3.3 过期键的判断方法

判断方法比较简单:

  1. 检查键是否存在于expires 字典:如果存在,那么取出键的过期时间;
  2. 检查当前UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,
    键未过期

4. 过期键的删除

过期键的删除有三种方法:

  1. 定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理 器自动执行键的删除操作。
  2. 惰性删除:放任键过期不管,但是在每次从dict 字典中取出键值时,要检查键是否过 期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。
  3. 定期删除:每隔一段时间,对expires 字典进行检查,删除里面的过期键。

定时删除对内存友好,能够保证过期的键被及时删除,不占用过多内存,但是对CPU不友好,需要为每一个键添加一个定时事件.
惰性删除对CPU友好,但是比较耗内存.
定期删除是上面两者的折中.

Redis 使用的过期键删除策略是惰性删除加上定期删除,这两个策略相互配合,可以很好地在合理利用CPU 时间和节约内存空间之间取得平衡。

5. 过期键与AOF/RDB

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值