前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis使用及源码剖析-10.Redis数据库-2021-1-27

Redis使用及源码剖析-10.Redis数据库-2021-1-27

作者头像
用户7719114
发布2022-02-22 13:40:56
3160
发布2022-02-22 13:40:56
举报
文章被收录于专栏:C++小白

文章目录

前言

本文对Redis的数据库文件进行简要介绍,包括数据库的选择,键的新建更新删除、Redis过期策略以及事件通知等。

一、数据库概念简介

1.数据库定义

Redis在redis.h中定义了数据库结构redisDb ,如下所示:

代码语言:javascript
复制
/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */

    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    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 */

    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

    // 数据库号码
    int id;                     /* Database ID */

    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */

} redisDb;

2.数据库切换

Redis服务端定义的结构体如下所示:

代码语言:javascript
复制
struct redisServer {
    // 数据库
    redisDb *db;
    int dbnum;                      /* Total number of configured DBs */
};

其中dbnum默认为16,redis服务端在初始化时会默认创建16个数据库,创建出来的数据库用db成员保存。各个数据库之间的数据完全独立,客户端默认的目标数据库为0号数据库,可以通过以下命令完成数据库切换:

代码语言:javascript
复制
SELECT 2 //切换目标数据库为2号数据库

3.数据库键空间

Redis是key-value型的内存数据库,其中每一个键值对都存在redisDb的dict词典中,其中dict被称为键空间。存放了列表键、哈希键和字符串键的键空间如下所示:

所以,每次键空间新建键、获取键的值、更新键内容、删除键的操作均是调用词典的API实现。

4.过期时间设置

Expire命令可以给一个key设置过期时间,ttl命令可以获取key的过期时间,过期时间到达后,redis会对过期的key进行删除。persist命令移除一个键的过期时间。

代码语言:javascript
复制
set wyl 520
expire wyl 10
ttl
10
persist wyl
ttl
-1

数据库中所有键的过期时间均保存在redisDb 的词典expires中,示意图如下:

5.过期删除策略

Redis过期删除策略包括惰性删除策略和定期删除策略两种。 a.惰性删除策略:每一次对数据库的键进行读写时都先检查key是否过期,过期则删除key并不操作。 b.定期删除策略:周期性的从数据库中选取一部分key检查是否过期,将过期的key删除。

6.数据库通知

Redis客户端可以订阅给定的频道或者模式,来从数据库获取通知。数据库通知分为两种,键空间通知和键事件通知。 键空间通知会告诉用户关注的键执行了什么命令,如下所示:

键事件通知会告诉用户关注的命令被那些键执行了,如下所示:

二、数据库相关API

1.数据库通知函数

键空间和键事件通知函数定义在notify.c中,定义如下:其中type为事件类型,event为事件名,key为键名称dbid是数据库所在ID。

代码语言:javascript
复制
/* The API provided to the rest of the Redis core is a simple function:
 *
 * notifyKeyspaceEvent(char *event, robj *key, int dbid);
 *
 * 'event' is a C string representing the event name.
 *
 * event 参数是一个字符串表示的事件名
 *
 * 'key' is a Redis object representing the key name.
 *
 * key 参数是一个 Redis 对象表示的键名
 *
 * 'dbid' is the database ID where the key lives.  
 *
 * dbid 参数为键所在的数据库
 */
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
    sds chan;
    robj *chanobj, *eventobj;
    int len = -1;
    char buf[24];

    /* If notifications for this class of events are off, return ASAP. */
    // 如果服务器配置为不发送 type 类型的通知,那么直接返回
    if (!(server.notify_keyspace_events & type)) return;

    // 事件的名字
    eventobj = createStringObject(event,strlen(event));

    /* __keyspace@<db>__:<key> <event> notifications. */
    // 发送键空间通知
    if (server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {

        // 构建频道对象
        chan = sdsnewlen("__keyspace@",11);
        len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, key->ptr);

        chanobj = createObject(REDIS_STRING, chan);

        // 通过 publish 命令发送通知
        pubsubPublishMessage(chanobj, eventobj);

        // 释放频道对象
        decrRefCount(chanobj);
    }

    /* __keyevente@<db>__:<event> <key> notifications. */
    // 发送键事件通知
    if (server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT) {

        // 构建频道对象
        chan = sdsnewlen("__keyevent@",11);
        // 如果在前面发送键空间通知的时候计算了 len ,那么它就不会是 -1
        // 这可以避免计算两次 buf 的长度
        if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, eventobj->ptr);

        chanobj = createObject(REDIS_STRING, chan);

        // 通过 publish 命令发送通知
        pubsubPublishMessage(chanobj, key);

        // 释放频道对象
        decrRefCount(chanobj);
    }

    // 释放事件对象
    decrRefCount(eventobj);
}

2.过期检查函数

过期检查函数定义在db.c中,在惰性过期策略中,每次执行键操作时都会调用它。每次定义如下:

代码语言:javascript
复制
/* Return the expire time of the specified key, or -1 if no expire
 * is associated with this key (i.e. the key is non volatile) 
 *
 * 返回给定 key 的过期时间。
 *
 * 如果键没有设置过期时间,那么返回 -1 。
 */
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;

    /* No expire? return ASAP */
    // 获取键的过期时间
    // 如果过期时间不存在,那么直接返回
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;

    /* The entry was found in the expire dict, this means it should also
     * be present in the main dict (safety check). */
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);

    // 返回过期时间
    return dictGetSignedIntegerVal(de);
}
/*
 * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
 *
 * 返回 0 表示键没有过期时间,或者键未过期。
 *
 * 返回 1 表示键已经因为过期而被删除了。
 */
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 */

    /* Don't expire anything while loading. It will be done later. */
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;

    /* If we are in the context of a Lua script, we claim that time is
     * blocked to when the Lua script started. This way a key can expire
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
    now = server.lua_caller ? server.lua_time_start : mstime();

    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller, 
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;

    // 运行到这里,表示键带有过期时间,并且服务器为主节点

    /* Return when this key has not expired */
    // 如果未过期,返回 0
    if (now <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;

    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);

    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);

    // 将过期键从数据库中删除
    return dbDelete(db,key);
}

3.切换目标数据库函数

通过select命令切换目标数据库函数实现如下:

代码语言:javascript
复制
/*
 * 将客户端的目标数据库切换为 id 所指定的数据库
 */
int selectDb(redisClient *c, int id) {

    // 确保 id 在正确范围内
    if (id < 0 || id >= server.dbnum)
        return REDIS_ERR;

    // 切换数据库(更新指针)
    c->db = &server.db[id];

    return REDIS_OK;
}
void selectCommand(redisClient *c) {
    long id;

    // 不合法的数据库号码
    if (getLongFromObjectOrReply(c, c->argv[1], &id,
        "invalid DB index") != REDIS_OK)
        return;

    if (server.cluster_enabled && id != 0) {
        addReplyError(c,"SELECT is not allowed in cluster mode");
        return;
    }

    // 切换数据库
    if (selectDb(c,id) == REDIS_ERR) {
        addReplyError(c,"invalid DB index");
    } else {
        addReply(c,shared.ok);
    }
}

4.设置过期时间函数

设置过期时间函数实现如下:

代码语言:javascript
复制
void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
/* 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).
 *
 * 这个函数是 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 命令的底层实现函数。
 *
 * 命令的第二个参数可能是绝对值,也可能是相对值。
 * 当执行 *AT 命令时, basetime 为 0 ,在其他情况下,它保存的就是当前的绝对时间。
 *
 * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
 * the argv[2] parameter. The basetime is always specified in milliseconds. 
 *
 * unit 用于指定 argv[2] (传入过期时间)的格式,
 * 它可以是 UNIT_SECONDS 或 UNIT_MILLISECONDS ,
 * basetime 参数则总是毫秒格式的。
 */
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */

    // 取出 when 参数
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;

    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;

    /* No key, return zero. */
    // 取出键
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }

    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
     * should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     *
     * 在载入数据时,或者服务器为附属节点时,
     * 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
     * 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
     *
     * Instead we take the other branch of the IF statement setting an expire
     * (possibly in the past) and wait for an explicit DEL from the master. 
     *
     * 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
     * 并且等待主节点发来 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {

        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据

        robj *aux;

        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL. */
        // 传播 DEL 命令
        aux = createStringObject("DEL",3);

        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);

        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);

        addReply(c, shared.cone);

        return;
    } else {

        // 设置键的过期时间
        // 如果服务器为附属节点,或者服务器正在载入,
        // 那么这个 when 有可能已经过期的
        setExpire(c->db,key,when);

        addReply(c,shared.cone);

        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);

        server.dirty++;

        return;
    }
}

5.查找key对应值函数

查找key对应值函数实现如下:

代码语言:javascript
复制
/*
 * 从数据库 db 中取出键 key 的值(对象)
 *
 * 如果 key 的值存在,那么返回该值;否则,返回 NULL 。
 */
robj *lookupKey(redisDb *db, robj *key) {

    // 查找键空间
    dictEntry *de = dictFind(db->dict,key->ptr);

    // 节点存在
    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. */
        // 更新时间信息(只在不存在子进程时执行,防止破坏 copy-on-write 机制)
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            val->lru = LRU_CLOCK();

        // 返回值
        return val;
    } else {

        // 节点不存在

        return NULL;
    }
}

总结

本文对Redis中数据库的实现做了简要介绍,如有错误,多多指教。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 前言
  • 一、数据库概念简介
    • 1.数据库定义
      • 2.数据库切换
        • 3.数据库键空间
          • 4.过期时间设置
            • 5.过期删除策略
              • 6.数据库通知
              • 二、数据库相关API
                • 1.数据库通知函数
                  • 2.过期检查函数
                    • 3.切换目标数据库函数
                      • 4.设置过期时间函数
                        • 5.查找key对应值函数
                        • 总结
                        相关产品与服务
                        云数据库 Redis®
                        腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档