redis源码分析与思考(九)——数据库结构以及基本命令的实现

    redis是基于内存存贮的数据库,所以所有的数据都是贮存在内存中,而且redis的架构是单线程的,如何在内存中创建并使得数据库高效的进行存贮显得尤为的困难。关于redis中数据库如何单线程的高效存贮值得我们来探讨,先来看看redis对数据库结构的定义:

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;   
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;
    // 正处于阻塞状态的键
    dict *blocking_keys;  
    // 可以解除阻塞的键
    dict *ready_keys;  
    // 正在被 WATCH 命令监视的键,如果被监视的键修改后会被标记为dirty数据,即脏数据
    dict *watched_keys;  
    struct evictionPoolEntry *eviction_pool;   
    // 数据库号码
    int id;                  
    // 数据库的键的平均生存时间
    long long avg_ttl;       
} redisDb;

    在数据库的定义中可以看到,dict字典保存着所有的键值对,也就是说,redis保存所有的数据都保存在dict字典中,dict中键保存着所有的键对象,也就是字符串对象,而dict中值保存着字符串对象、列表对象、哈希对象、集合对象以及有序集合对象。之所以采用字典来保存所有的键,是因为它查找键的时间复杂度为O(1)。

键空间管理

    因为redis中所有数据都是以键值对的形式存放,所以redis中对数据的命令操作都是基于字典中键的操作展开的,数据库对键的管理操作如下:

//对某个键取值操作
robj *lookupKey(redisDb *db, robj *key) {
    // 查找键空间
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 节点存在
    if (de) {
        // 取出值
        robj *val = dictGetVal(de);
        // 更新时间信息(只在不存在子进程时执行,防止破坏 copy-on-write 机制),费时操作
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            val->lru = LRU_CLOCK();
        // 返回值
        return val;
    } else {
        // 节点不存在
        return NULL;
    }
}

//为执行读取操作而取出键 key 在数据库 db 中的值。并根据是否成功找到值,更新服务器的命中/不命中信息。找到时返回值对象,没找到返回 NULL 。
robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;
    // 检查 key 释放已经过期
    expireIfNeeded(db,key);
    // 从数据库中取出键的值
    val = lookupKey(db,key);
    // 更新命中/不命中信息
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;

    // 返回值
    return val;
}

 //为执行写入操作而取出键 key 在数据库 db 中的值。 找到时返回值对象,没找到返回 NULL 。
robj *lookupKeyWrite(redisDb *db, robj *key) {
    // 删除过期键
    expireIfNeeded(db,key);
    // 查找并返回 key 的值对象
    return lookupKey(db,key);
}

//增加键
void dbAdd(redisDb *db, robj *key, robj *val) {
    // 复制键名
    sds copy = sdsdup(key->ptr);
    // 尝试添加键值对
    int retval = dictAdd(db->dict, copy, val);
    // 如果键已经存在,那么停止
    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    // 如果开启了集群模式,那么将键保存到槽里面
    if (server.cluster_enabled) slotToKeyAdd(key);
 }

 //为已存在的键关联一个新值。调用者负责对新值 val 的引用计数进行增加。 如果键不存在,那么函数停止。
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);
}

//设置键对应的值,不管键是否存在
void setKey(redisDb *db, robj *key, robj *val) {
    // 添加或覆写数据库中的键值对
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    //对值对象引用计数加1
    incrRefCount(val);
    // 移除键的过期时间
    removeExpire(db,key);
    // 发送键修改通知
    signalModifiedKey(db,key);
}

//删除键
//从数据库中删除给定的键,键的值,以及键的过期时间。
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;
}

/*
 * 将客户端的目标数据库切换为 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;
}

//随机一个不过期的键
robj *dbRandomKey(redisDb *db) {
    dictEntry *de;
    while(1) {
        sds key;
        robj *keyobj;
        // 从键空间中随机取出一个键节点
        de = dictGetRandomKey(db->dict);
        // 数据库为空
        if (de == NULL) return NULL;
        // 取出键
        key = dictGetKey(de);
        // 为键创建一个字符串对象,对象的值为键的名字
        keyobj = createStringObject(key,sdslen(key));
        // 检查键是否带有过期时间
        if (dictFind(db->expires,key)) {
            // 如果键已经过期,那么将它删除,并继续随机下个键
            if (expireIfNeeded(db,keyobj)) {
                decrRefCount(keyobj);
                continue; /* search for another key. This expired. */
            }
        }
        // 返回被随机到的键(的名字)
        return keyobj;
    }
}

    在对键进行读写操作时,都会检查键是否过期,当该键过期时,不进行操作,而且访问每个键后,都会更新最后一次访问该键的时间。下面给出清除整个数据库、切换数据库等命令的实现:

/*
 * 清空客户端指定的数据库命令,FLUSHDB命令
 */
void flushdbCommand(redisClient *c) {
    server.dirty += dictSize(c->db->dict);
    // 发送通知
    signalFlushedDb(c->db->id);
    // 清空指定数据库中的 dict 和 expires 字典
    dictEmpty(c->db->dict,NULL);
    dictEmpty(c->db->expires,NULL);
    // 如果开启了集群模式,那么还要移除槽记录
    if (server.cluster_enabled) slotToKeyFlush();
    //发送修改成功到客户端
    addReply(c,shared.ok);
}

/*
 * 清空服务器中的所有数据库命令,FLUSHALL命令
 */
void flushallCommand(redisClient *c) {
    // 发送通知
    signalFlushedDb(-1);
    // 清空所有数据库
    server.dirty += emptyDb(NULL);
    addReply(c,shared.ok);
    // 如果正在保存新的 RDB ,那么取消保存操作
    if (server.rdb_child_pid != -1) {
        kill(server.rdb_child_pid,SIGUSR1);
        rdbRemoveTempFile(server.rdb_child_pid);
    }
    // 更新 RDB 文件
    if (server.saveparamslen > 0) {
        // rdbSave() 会清空服务器的 dirty 属性
        // 但为了确保 FLUSHALL 命令会被正常传播,
        // 程序需要保存并在 rdbSave() 调用之后还原服务器的 dirty 属性
        int saved_dirty = server.dirty;
        rdbSave(server.rdb_filename);
        server.dirty = saved_dirty;
    }
    server.dirty++;
}

//删除命令,DEL命令
void delCommand(redisClient *c) {
    int deleted = 0, j;
    // 遍历所有输入键
    for (j = 1; j < c->argc; j++) {
        // 先删除过期的键
        expireIfNeeded(c->db,c->argv[j]);
        // 尝试删除键
        if (dbDelete(c->db,c->argv[j])) {
            // 删除键成功,发送通知
            signalModifiedKey(c->db,c->argv[j]);
            notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty++;
            // 成功删除才增加 deleted 计数器的值
            deleted++;
        }
    }
    // 发送返被删除键的数量到客户端
    addReplyLongLong(c,deleted);
}

//检查是否键存在命令,EXITS命令
void existsCommand(redisClient *c) {
    // 检查键是否已经过期,如果已过期的话,那么将它删除
    // 这可以避免已过期的键被误认为存在
    expireIfNeeded(c->db,c->argv[1]);
    // 在数据库中查找
    if (dbExists(c->db,c->argv[1])) {
        addReply(c, shared.cone);
    } else {
        addReply(c, shared.czero);
    }
}

//切换数据库命令,SELECT命令
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);
    }
}

//随机返回一个不过期的键命令,RANDOMKEY命令
void randomkeyCommand(redisClient *c) {
    robj *key;
    // 随机返回键
    if ((key = dbRandomKey(c->db)) == NULL) {
        addReply(c,shared.nullbulk);
        return;
    }
    addReplyBulk(c,key);
    //键对象引用计数加1
    decrRefCount(key);
}

    在redis源码中以Command结尾的函数,是其对应的命令的实现。总的来说,对键空间的维护操作有如下几点:

  1. 在读写一个键时,都会对键进行读取操作,服务器会根据键是否存在来更新键空间的命中与不命中次数;
  2. 读取一个键时,都会更新LRU,即最后一次使用时间,通过这个属性可计算该键闲置的时长,当服务器的内存达到上限时,会优先回收闲置时间长的键;
  3. 读取一个键时,都会检查该键是否过期;如果一个键是处在被WATCH中,那么只要修改了该键,那么该键会被认定为dirty数据,会引发持久化操作。

键的生存时间(Time to Live,TTL)

    在redis中,为了防止键的长久不用造成内存的浪费,引进了一个字典expires,该字典记录键的过期时间(设置了过期时间的键),当键到了其过期时间时,redis会自动删除这个键。对键生存时间的底层操作:

/*
 * 移除键 key 的过期时间
 */
int removeExpire(redisDb *db, robj *key) {
    // 确保键带有过期时间
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    // 删除过期时间
    return dictDelete(db->expires,key->ptr) == DICT_OK;
}

/*
 * 将键 key 的过期时间设为 when
 */
void setExpire(redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;
    // 取出键
    kde = dictFind(db->dict,key->ptr);
    redisAssertWithInfo(NULL,key,kde != NULL);
    // 根据键取出键的过期时间
    de = dictReplaceRaw(db->expires,dictGetKey(kde));
    // 设置键的过期时间
    // 这里是直接使用整数值来保存过期时间,不是用 INT 编码的 String 对象
    dictSetSignedIntegerVal(de,when);
}

 //返回给定 key 的过期时间。 如果键没有设置过期时间,那么返回 -1 。
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;
    // 获取键的过期时间
    // 如果过期时间不存在,那么直接返回
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    // 返回过期时间
    return dictGetSignedIntegerVal(de);
}

/* 
 * 将过期时间传播到附属节点和 AOF 文件。主节点中过期时,
 * 主节点会向所有附属节点和 AOF 文件传播一个显式的 DEL 命令。
 * 这种做法使得对键的过期可以集中在一处处理,
 * 因为 AOF 以及主节点和附属节点之间的链接,都可以保证操作的执行顺序,
 * 所以即使有写操作对过期键执行,所有数据都还是 consistent 的。
 */
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]);
}

/*
 * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
 * 返回 0 表示键没有过期时间,或者键未过期。
 * 返回 1 表示键已经因为过期而被删除了。
 */
int expireIfNeeded(redisDb *db, robj *key) {
    // 取出键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 没有过期时间
    if (when < 0) return 0; 
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;
    now = server.lua_caller ? server.lua_time_start : mstime();
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    //简单来说,判断该服务器是否是主服务器,不是则返回1,且不删除过期键,
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    // 如果未过期,返回 0
    if (now <= when) return 0;
    //服务器中的过期键数加一
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
}

    其中注意的是expireIfNeeded函数,如果该数据库所对应的服务器是从服务器,而不是主服务器,那么删除过期键时不对其真正进行删除,等到主服务器删除该键并传播删除命令时,从服务器才删除,这样就保证了主从服务器的数据一致性。下面给出对键生存时间操作命令的实现:

//设置键的生存时间
/* 这个函数是 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 命令的底层实现函数。
 * 命令的第二个参数可能是绝对值,也可能是相对值。
 * 当执行 *AT 命令时, basetime 为 0 ,在其他情况下,它保存的就是当前的绝对时间。 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; 
    // 取出 when 参数
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;
    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;
    // 取出键
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }
    /* 
     * 在载入数据时,或者服务器为附属节点时,
     * 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
     * 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
     * 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
     * 并且等待主节点发来 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {
        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据
        robj *aux;
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
        // 传播 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);
        //脏数据加1
        server.dirty++;
        return;
    }
}

//移除键的过期时间,即PERSIST命令
void persistCommand(redisClient *c) {
    dictEntry *de;
    // 取出键
    de = dictFind(c->db->dict,c->argv[1]->ptr);
    if (de == NULL) {
        // 键没有过期时间
        addReply(c,shared.czero);
    } else {
        // 键带有过期时间,那么将它移除
        if (removeExpire(c->db,c->argv[1])) {
            addReply(c,shared.cone);
            server.dirty++;
        // 键已经是持久的了
        } else {
            addReply(c,shared.czero);
        }
    }
}

/*
 * 返回键的剩余生存时间。
 * output_ms 指定返回值的格式:
 *  - 为 1 时,返回毫秒,即PTTL命令
 *  - 为 0 时,返回秒,即TTL命令
 */
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;
    // 取出键
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    // 取出过期时间
    expire = getExpire(c->db,c->argv[1]);
    if (expire != -1) {
        // 计算剩余生存时间
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }
    if (ttl == -1) {
        // 键是持久的
        addReplyLongLong(c,-1);
    } else {
        // 返回 TTL 
        // (ttl+500)/1000 计算的是渐近秒数
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

    如果是主节点命令且键已过期,那么进行传播del命令进行删除,否则进行键的生存时间赋值操作。

数据库的其它命令实现

    下面列出常用的对键操作命令的实现:

//DBSIZE命令
void dbsizeCommand(redisClient *c) {
    addReplyLongLong(c,dictSize(c->db->dict));
}

//返回最后一个保存的键
void lastsaveCommand(redisClient *c) {
    addReplyLongLong(c,server.lastsave);
}

//判断哪种类型,TYPE命令
void typeCommand(redisClient *c) {
    robj *o;
    char *type;
    o = lookupKeyRead(c->db,c->argv[1]);
    if (o == NULL) {
        type = "none";
    } else {
        switch(o->type) {
        case REDIS_STRING: type = "string"; break;
        case REDIS_LIST: type = "list"; break;
        case REDIS_SET: type = "set"; break;
        case REDIS_ZSET: type = "zset"; break;
        case REDIS_HASH: type = "hash"; break;
        default: type = "unknown"; break;
        }
    }
    addReplyStatus(c,type);
}

//关闭客户端
void shutdownCommand(redisClient *c) {
    int flags = 0;
    if (c->argc > 2) {
        addReply(c,shared.syntaxerr);
        return;
    } else if (c->argc == 2) {
        // 停机时不进行保存
        if (!strcasecmp(c->argv[1]->ptr,"nosave")) {
            flags |= REDIS_SHUTDOWN_NOSAVE;
        // 停机时进行保存
        } else if (!strcasecmp(c->argv[1]->ptr,"save")) {
            flags |= REDIS_SHUTDOWN_SAVE;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    /* When SHUTDOWN is called while the server is loading a dataset in
     * memory we need to make sure no attempt is performed to save
     * the dataset on shutdown (otherwise it could overwrite the current DB
     * with half-read data).
     *
     * Also when in Sentinel mode clear the SAVE flag and force NOSAVE. */
    if (server.loading || server.sentinel_mode)
        flags = (flags & ~REDIS_SHUTDOWN_SAVE) | REDIS_SHUTDOWN_NOSAVE;
    if (prepareForShutdown(flags) == REDIS_OK) exit(0);
    addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
}

//修改键名,RENAME命令
void renameGenericCommand(redisClient *c, int nx) {
    robj *o;
    long long expire;
    /* To use the same key as src and dst is probably an error */
    // 来源键和目标键不能相同
    if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) {
        addReply(c,shared.sameobjecterr);
        return;
    }
    // 取出来源键
    if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
        return;
    // 增加引用计数,因为后面目标键也会引用这个对象
    // 如果不增加的话,当来源键被删除时,这个值对象也会被删除
    incrRefCount(o);
    // 取出来源键的过期时间
    expire = getExpire(c->db,c->argv[1]);
    // 检查目标键是否存在
    if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
        // 如果目标键存在,并且执行的是 RENAMENX ,那么直接返回
        if (nx) {
            decrRefCount(o);
            addReply(c,shared.czero);
            return;
        }
        // 如果执行的是 RENAME ,那么删除已有的目标键
        /* Overwrite: delete the old key before creating the new one
         * with the same name. */
        dbDelete(c->db,c->argv[2]);
    }
    // 将来源键的值对象和目标键进行关联
    dbAdd(c->db,c->argv[2],o);
    // 如果有过期时间,那么为目标键设置过期时间
    if (expire != -1) setExpire(c->db,c->argv[2],expire);
    // 删除来源键
    dbDelete(c->db,c->argv[1]);
    signalModifiedKey(c->db,c->argv[1]);
    signalModifiedKey(c->db,c->argv[2]);
    notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"rename_from",
        c->argv[1],c->db->id);
    notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"rename_to",
        c->argv[2],c->db->id);
    server.dirty++;
    addReply(c,nx ? shared.cone : shared.ok);
}

//MOVE命令实现,将某个键迁移到另一个数据库
void moveCommand(redisClient *c) {
    robj *o;
    redisDb *src, *dst;
    int srcid;
    if (server.cluster_enabled) {
        addReplyError(c,"MOVE is not allowed in cluster mode");
        return;
    }
    /* Obtain source and target DB pointers */
    // 源数据库
    src = c->db;
    // 源数据库的 id
    srcid = c->db->id;
    // 切换到目标数据库
    if (selectDb(c,atoi(c->argv[2]->ptr)) == REDIS_ERR) {
        addReply(c,shared.outofrangeerr);
        return;
    }
    // 目标数据库
    dst = c->db;
    // 切换回源数据库
    selectDb(c,srcid); /* Back to the source DB */
    /* If the user is moving using as target the same
     * DB as the source DB it is probably an error. */
    // 如果源数据库和目标数据库相等,那么返回错误
    if (src == dst) {
        addReply(c,shared.sameobjecterr);
        return;
    }
    /* Check if the element exists and get a reference */
    // 取出要移动的对象
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (!o) {
        addReply(c,shared.czero);
        return;
    }
    /* Return zero if the key already exists in the target DB */
    // 如果键已经存在于目标数据库,那么返回
    if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
        addReply(c,shared.czero);
        return;
    }
    // 将键添加到目标数据库中
    dbAdd(dst,c->argv[1],o);
    // 增加对对象的引用计数,避免接下来在源数据库中删除时 o 被清理
    incrRefCount(o);
    /* OK! key moved, free the entry in the source DB */
    // 将键从源数据库中返回
    dbDelete(src,c->argv[1]);
    server.dirty++;
    addReply(c,shared.cone);
}

总结

  1. redis所有的键值对都存在一个dict字典中,键的过期时间存在expires字典中
  2. 从服务器发现了过期键也不会删除,等待主服务器的del命令后才删除
  3. redis采用dirty属性来记录被WATCH命令监视的键的修改情况,并引发持久化操作
  4. 一个服务器有多个数据库,键之间可迁移。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值