redis 源码系列(13):我的地盘我做主--- database

redis 内部也像传统的关系型数据库一样,有 database 的概念。主要用于实现数据隔离,这个在生产环境中是非常必要的。多个进程共同使用一个 redis 服务,如果没有数据隔离的机制,对于开发、运维都会有很大的挑战。

今天来看一下 redis 中 database 的实现

database

redisDb

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;

回顾之前的 redis 初始化、以及构造 client 相关的代码,我们知道每个 client 都会有一个指定的 db,该 client 后续所有的操作,都是发生在其指定的 database 内的。

查找

要在一个 database 中查找 key 只需要在其 dict 内查找即可

robj *lookupKey(redisDb *db, robj *key) {
    // 查找键空间
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 节点存在
    if (de) {     
        // 取出值
        robj *val = dictGetVal(de);
        // 如果有子进程,不更新 lru 时间戳,避免 COW
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            val->lru = LRU_CLOCK();
        // 返回值
        return val;
    } else {
        // 节点不存在
        return NULL;
    }
}

添加

void dbAdd(redisDb *db, robj *key, robj *val) {
    // 复制键名
    sds copy = sdsdup(key->ptr);
    // 尝试添加键值对
    int retval = dictAdd(db->dict, copy, val);
    // 键不能已经存在于 dict
    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    // 如果开启了集群模式,那么将键保存到槽里面
    if (server.cluster_enabled) slotToKeyAdd(key);
 }
void dbOverwrite(redisDb *db, robj *key, robj *val) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    
    // 节点必须存在,否则中止
    redisAssertWithInfo(NULL,key,de != NULL);

    // 覆写旧值
    dictReplace(db->dict, key->ptr, val);
}
// high level 的 set 操作
void setKey(redisDb *db, robj *key, robj *val) {
    // 添加或覆写数据库中的键值对
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    // 增加对 val 的引用计数
    incrRefCount(val);
    // 移除键的过期时间
    removeExpire(db,key);
    // 发送键修改通知
    signalModifiedKey(db,key);
}

void signalModifiedKey(redisDb *db, robj *key) {
    touchWatchedKey(db,key);
}

注意我们一定要使用 sdsdup 对 key 进行 deep copy。

删除

int dbDelete(redisDb *db, robj *key) {
    // 删除键的过期时间
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    // 删除键值对
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        // 如果开启了集群模式,那么从槽中删除给定的键
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        // 键不存在
        return 0;
    }
}

清空数据库

long long emptyDb(void(callback)(void*)) {
    int j;
    long long removed = 0;
    // 清空所有数据库
    for (j = 0; j < server.dbnum; j++) {
        // 记录被删除键的数量
        removed += dictSize(server.db[j].dict);
        // 删除所有键值对
        dictEmpty(server.db[j].dict,callback);
        // 删除所有键的过期时间
        dictEmpty(server.db[j].expires,callback);
    }
    // 如果开启了集群模式,那么还要移除槽记录
    if (server.cluster_enabled) slotToKeyFlush();
    // 返回键的数量
    return removed;
}

键过期

redis 支持客户端指定缓存的过期时间,这一点对于一个缓存服务分外重要。因为 redis 往往作为一个缓存服务使用,但是在分布式架构中,很容易出现缓存与数据库数据出现不一致的情况,能让缓存自动过期,给了我们一种简单的修正方法。

来看一下 expire 相关命令如何实现:

// expire command 族的底层实现函数
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. */
    // param 是 when 参数,获取其底层数值
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;
    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    // redis 提供使用相对时间和绝对时间两种方式设定过期时间
    // param 使用绝对时间时,basetime == 0
    // param 使用相对时间时,basetime == 当前时间 
    when += basetime;
		// 如果 database 中 key 不存在则直接返回
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }

    if (when <= mstime() && !server.loading && !server.masterhost) {
        // 可能在接受到 expire 命令时候,key 的 ttl(time to live) 已经为负
        // 代表 key 已经过期,在本节点没有在载入数据而去不是从节点时,需要立即删除 key
        robj *aux;
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
        // 构造 DEL command,传播 DEL 命令到从节点
        aux = createStringObject("DEL",3);
        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);
        signalModifiedKey(c->db,key);
        // notify 与 pub/sub 相关,先略过
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
        // 设置键的过期时间,注意这个 key 还是可能是过期的,但是这种情况下
        // 如果我们是从节点,等待 master 发送 DEL 过来即可,不要自己去删除
        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;
    }
}

// 若干秒后 key 过期(相对时间)
void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
// 在指定的时间戳达到以后,key 过期(绝对时间)
void expireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_SECONDS);
}

在 database 查询某个键的时候,redis 会检查 key 是否过期:

void propagateExpire(redisDb *db, robj *key) {
    robj *argv[2];
    // 构造一个 DEL key 命令
    argv[0] = shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
    // 传播到 AOF 
    if (server.aof_state != REDIS_AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    // 传播到所有附属节点
    replicationFeedSlaves(server.slaves,db->id,argv,2);
    decrRefCount(argv[0]);
    decrRefCount(argv[1]);
}

long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;
		// 未设置过期时间,返回 -1
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;

    // 设置了过期时间的 key 肯定是存在于 database 键空间的
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    // 返回过期时间
    return dictGetSignedIntegerVal(de);
}

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;

    // 如果是在执行一个 lua 脚本,那么我们只在脚本开始的时候,记录一次时间
    // 后续脚本中所有 key 的过期都按照脚本开始时间来计算
    now = server.lua_caller ? server.lua_time_start : mstime();

    // 如果我们只是一个从节点,我们不会主动的去 expire 删除 key,而是等待
    // master 发送 DEL 命令过来,所以立即返回
    if (server.masterhost != NULL) return now > when;

    // 如果未过期,返回 0
    if (now <= when) return 0;
		// 删除这个 key
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
}

总结

  1. database 提供数据隔离功能,每一个 client 的操作都是与某一个 database 绑定的
  2. 当执行一个 lua 脚本时,key 是否过期都是按照 lua 脚本开始执行时时间计算的,而不会持续更新当前时间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值