看了一些关于redis 的相关文章,例如redis为什么这么快、redis的原理、redis的数据结构这些,但其只是从整体的结构来说明,并没有梳理源码的具体流程,但我不是很喜欢一些黑盒的东西,所以我们这一篇就通过跑redis的源码,来追踪redis源码中的一些数据结构。这篇文章的源码是基于redis 3.0版本,同时源码是直接从github上面clone下来的别人已经处理好的redis代码(windows平台),地址为:https://github.com/htw0056/redis-3.0-annotated-cmake-in-clion。感谢这位前辈。

首先我们启动服务端,然后再启动客户端,通过客户端输入命令我们来跟踪其的主要结构,下面就正式开始。

redis源码分析系列 redis源码解读_源码解读

我们通过客户端来设置值,然后来追踪其的执行。

redis源码分析系列 redis源码解读_数据库_02

一、redisClient关联内容

redis源码分析系列 redis源码解读_源码解读_03

其首先是从redis.c的main还是开始的,然后通过在main函数中调用aeMain(aeEventLoop *eventLoop)来处理事件(redis使用的是IO多路复用epoll)。当然这里以及之后会进行各种内存、数据结构的初始化等。我们就不具体分析这种了(对于c语言我也只是能看懂大概的内容,具体细节也不是很明白)。但我们通过源码debug还是能明白大体的结构的。

1、整体执行流程

/*
 * 事件处理器的主循环
 */
void aeMain(aeEventLoop *eventLoop) {

    eventLoop->stop = 0;

    while (!eventLoop->stop) {

        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

下面我们就直接到processCommand方法,来看其对于输入命令的具体执行。

int processCommand(redisClient *c) {
		.............
  		
    // 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作
    if (server.maxmemory) {
        // 如果内存已超过限制,那么尝试通过删除过期键来释放内存
        int retval = freeMemoryIfNeeded();
        // 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
        // 并且前面的内存释放失败的话
        // 那么向客户端返回内存错误
        if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
            flagTransaction(c);
            addReply(c, shared.oomerr);
            return REDIS_OK;
        }
    }
		........
    /* Don't accept write commands if this is a read only slave. But
     * accept write commands if this is our master. */
    // 如果这个服务器是一个只读 slave 的话,那么拒绝执行写命令
    if (server.masterhost && server.repl_slave_ro &&
        !(c->flags & REDIS_MASTER) &&
        c->cmd->flags & REDIS_CMD_WRITE)
    {
        addReply(c, shared.roslaveerr);
        return REDIS_OK;
    }
		............
        // 执行命令
        call(c,REDIS_CALL_FULL);
		..........
    return REDIS_OK;
}

这里我们省略了很多内容,值保留了部分内容。

2、结构体内容

1)、redisClient结构体

首先我们来看下redisClient结构体,这个就是redis的客户端链接命令处理:

typedef struct redisClient {

    // 套接字描述符
    int fd;

    // 当前正在使用的数据库
    redisDb *db;

    // 当前正在使用的数据库的 id (号码)
    int dictid;

    // 客户端的名字
    robj *name;             /* As set by CLIENT SETNAME */
		.........

} redisClient;

redis源码分析系列 redis源码解读_源码解读_04

这里是直接有redisDB,然后dictid是表示当前使用的是哪个redisDB,例如当前就是使用的0号库。然后在redisDb中,就有真正存放添加的数据了。

2)、redisDb结构体

typedef struct redisDb {

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

    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
		......
    // 数据库号码
    int id;                     /* Database ID */

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

} redisDb;

这个就是redis数据库的结构体了,类似于mysql的数据库的概念,不过其的数据库名称就是id标识,表示是第几号库。

3)、dict结构体

typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

这个就是字典表。然后数据就是存在两张hash表中ht[2]中**(redis不管要存入的是哪种类型例如stringlisthash这些,都是放在这个hash表中**、(然后这里就是文章上说的,redis为什么这么快的第二个答案了(因为使用hash结构,其的查找会很快,第一个快的原因我认为是使用的内存),同时要注意这些类型存在redis内部又是其他具体设置的结构体、例如LINKEDLIST常规链表、ZIPLIST压缩列表、SKIPLIST跳表),这个我们后面再来看。

然后这里dictht ht[2]之所以是两张表,是用来扩容使用的,在渐进式hash扩容期间其是会使用两张表的,然后在ht[0]中获取不到,就会去ht[1]中找**(关于渐进式hash我们后面也会说明)(这个也可以是速度快的第三个原因)**。然后这里的rehashidx就用来标识当前是不是在渐进式hash扩容期间。

4)、dictht &dictEntry 结构体

typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

这个就是hash表数组(客户端的设置获取命令最终都要落到table来。),然后used表示当前包含的节点,而dictEntry就是对应的实体名称。

可以看到目前我们的key是有30个。

redis源码分析系列 redis源码解读_redis数据结构_05

5)、整体数据介绍

然后我们再来看下redisClient关联的db数据:

redis源码分析系列 redis源码解读_redis_06

可以看到其当前使用的个数是30个,同时其也要扩容了。但现在还没有扩容rehashidx-1

3、processCommand执行步骤

1)、查询检查命令

这里就是检查查看有没有我们当前输入的命令,如果没有就给出提示返回unknown command

c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (!c->cmd) {
    // 没找到指定的命令
    flagTransaction(c);
    addReplyErrorFormat(c,"unknown command '%s'",
        (char*)c->argv[0]->ptr);
    return REDIS_OK;
}
struct redisCommand *lookupCommand(sds name) {
    return dictFetchValue(server.commands, name);
}
void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    // T = O(1)
    he = dictFind(d,key);

    return he ? dictGetVal(he) : NULL;
}

redis源码分析系列 redis源码解读_源码解读_07

我们目前是set命令。

2)、server.commands填充逻辑

首这个字典的参数化就是在populateCommandTable方法遍历添加的。

void populateCommandTable(void) {
    int j;

    // 命令的数量
    int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);

    for (j = 0; j < numcommands; j++) {
        
        // 指定命令
        struct redisCommand *c = redisCommandTable+j;

        // 取出字符串 FLAG
        char *f = c->sflags;
			...........
        // 将命令关联到命令表
        retval1 = dictAdd(server.commands, sdsnew(c->name), c);
		.........
    }
}
struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"r",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,2,"r",0,NULL,1,1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getbit",getbitCommand,3,"r",0,NULL,1,1,1,0,0},
    {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"incr",incrCommand,2,"wm",0,NULL,1,1,1,0,0},
    {"decr",decrCommand,2,"wm",0,NULL,1,1,1,0,0},
    {"mget",mgetCommand,-2,"r",0,NULL,1,-1,1,0,0},
    {"rpush",rpushCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"lpush",lpushCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"rpushx",rpushxCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"lpushx",lpushxCommand,3,"wm",0,NULL,1,1,1,0,0},
  		............
    {"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0}
};

这里面就是所有的redis命令。

3)、字典添加的逻辑(dictAddRaw)

int dictAdd(dict *d, void *key, void *val)
{
    // 尝试添加键到字典,并返回包含了这个键的新哈希节点
    // T = O(N)
    dictEntry *entry = dictAddRaw(d,key);

    // 键已存在,添加失败
    if (!entry) return DICT_ERR;

    // 键不存在,设置节点的值
    // T = O(1)
    dictSetVal(d, entry, val);

    // 添加成功
    return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    // 如果条件允许的话,进行单步 rehash
    // T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 计算键在哈希表中的索引值
    // 如果值为 -1 ,那么表示键已经存在
    // T = O(N)
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    // T = O(1)
    /* Allocate the memory and store the new entry */
    // 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
    // 否则,将新键添加到 0 号哈希表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 为新节点分配空间
    entry = zmalloc(sizeof(*entry));
    // 将新节点插入到链表表头
    entry->next = ht->table[index];
    ht->table[index] = entry;
    // 更新哈希表已使用节点数量
    ht->used++;

    /* Set the hash entry fields. */
    // 设置新节点的键
    // T = O(1)
    dictSetKey(d, entry, key);

    return entry;
}

这里具体有3步:首先看需不需要单步渐进hash赋值,如果需要(扩容的时候才需要),就渐进hash(其的步长是1(也就是一次只一定hash表的一个位置的节点链表到另一张表)),然后创建分配dictEntry,再计算其的hash表index使用头插法将其添加hash表中。这个是命令的字典表。当我们的设置字典表也是类似这个逻辑。

4)、内存清除

/* Handle the maxmemory directive.
 *
 * First we try to free some memory if possible (if there are volatile
 * keys in the dataset). If there are not the only thing we can do
 * is returning an error. */
// 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作
if (server.maxmemory) {
    // 如果内存已超过限制,那么尝试通过删除过期键来释放内存
    int retval = freeMemoryIfNeeded();
    // 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
    // 并且前面的内存释放失败的话
    // 那么向客户端返回内存错误
    if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
        flagTransaction(c);
        addReply(c, shared.oomerr);
        return REDIS_OK;
    }
}

在正式执行命令设置内容之前,我们需要检查是不是已经达到最大内存了,如果到了,我们就需要根据内存淘汰策略其起来一些值了:

int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);
		............
      // 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; /* We need to free memory, but policy forbids. */
    /* Compute how much memory we need to free. */
    // 计算需要释放多少字节的内存
    mem_tofree = mem_used - server.maxmemory;
    // 初始化已释放内存的字节数为 0
    mem_freed = 0;
    // 根据 maxmemory 策略,
    // 遍历字典,释放内存并记录被释放内存的字节数
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;
        // 遍历所有字典
        for (j = 0; j < server.dbnum; j++) {
            long bestval = 0; /* just to prevent warning */
            sds bestkey = NULL;
            dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;

            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                // 如果策略是 allkeys-lru 或者 allkeys-random 
                // 那么淘汰的目标为所有数据库键
                dict = server.db[j].dict;
            } else {
                // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl 
                // 那么淘汰的目标为带过期时间的数据库键
                dict = server.db[j].expires;
            }

            // 跳过空字典
            if (dictSize(dict) == 0) continue;

            /* volatile-random and allkeys-random policy */
            // 如果使用的是随机策略,那么从目标字典中随机选出键
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);
                bestkey = dictGetKey(de);
            }
            /* volatile-lru and allkeys-lru policy */
            // 如果使用的是 LRU 策略,
            // 那么从一集 sample 键中选出 IDLE 时间最长的那个键
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                	............
            }
            /* volatile-ttl */
            // 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
						.........
                }
            }
            /* Finally remove the selected key. */
            // 删除被选中的键
            if (bestkey) {
                ......
                dbDelete(db,keyobj);
				.......
            }
        }
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }

    return REDIS_OK;
}

这个首先是遍历所有的db,然后根据淘汰策略,如果是以所有key(ALLKEYS)为目标其是直接server.db[j].dict,如过不是(就是过期key)server.db[j].expires,然后再是具体的策略处理了:

/* Redis maxmemory strategies */
#define REDIS_MAXMEMORY_VOLATILE_LRU 0
#define REDIS_MAXMEMORY_VOLATILE_TTL 1
#define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
#define REDIS_MAXMEMORY_ALLKEYS_LRU 3
#define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
#define REDIS_MAXMEMORY_NO_EVICTION 5
#define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION

看以看到默认的策略就是REDIS_MAXMEMORY_NO_EVICTION,永不淘汰,然后直接就会直接返回REDIS_ERR

5)、执行具体的命令调用(例如set)

redis源码分析系列 redis源码解读_redis_08

再之后,就是正式调用了。

void call(redisClient *c, int flags) {
    // start 记录命令开始执行的时间
    long long dirty, start, duration;
    // 记录命令开始执行前的 FLAG
    int client_old_flags = c->flags;
	...........
    // 保留旧 dirty 计数器值
    dirty = server.dirty;
    // 计算命令开始执行的时间
    start = ustime();
    // 执行实现函数
    c->cmd->proc(c);
    // 计算命令执行耗费的时间
    duration = ustime()-start;
    // 计算命令执行之后的 dirty 值
    dirty = server.dirty-dirty;
	.........
    server.stat_numcommands++;
}

这里正在调永的是c->cmd->proc(c);,这个是与你具体的命令相关的,当前是set命令其就会到:

/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(redisClient *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = REDIS_SET_NO_FLAGS;
		.........
    // 尝试对值对象进行编码
    c->argv[2] = tryObjectEncoding(c->argv[2]);

    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
void setGenericCommand(redisClient *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {

    long long milliseconds = 0; /* initialized to avoid any harmness warning */
		..........
    // 将键值关联到数据库
    setKey(c->db,key,val);

    // 将数据库设为脏
    server.dirty++;
    // 为键设置过期时间
    if (expire) setExpire(c->db,key,mstime()+milliseconds);
        // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",key,c->db->id);
		.......
    // 设置成功,向客户端发送回复
    // 回复的内容由 ok_reply 决定
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

这里先是通过setKey(c->db,key,val)将其设置到hash表中(获取redisClient对应的db),然后将server.dirty自增(用于rdb同步)。

// 检查是否有某个保存条件已经满足了
if (server.dirty >= sp->changes &&
    server.unixtime-server.lastsave > sp->seconds &&
    (server.unixtime-server.lastbgsave_try >
     REDIS_BGSAVE_RETRY_DELAY ||
     server.lastbgsave_status == REDIS_OK))
{
    redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
        sp->changes, (int)sp->seconds);
    // 执行 BGSAVE
    rdbSaveBackground(server.rdb_filename);
    break;
}

例如这里就是判断修改时间间隔,以及在这个时间内进行了多少次修改,条件满足的话,就通过rdbSaveBackground(server.rdb_filename)来解析rdb快照备份。

然后下面我们就来具体说明下关于命令的hash表字典的设置(setKey(c->db,key,val))、获取,以及rehash 的处理。

二、具体的命令设值 setKey(c->db,key,val)

redis源码分析系列 redis源码解读_源码解读_09

我们当前的命令是set t5 v4

void setKey(redisDb *db, robj *key, robj *val) {

    // 添加或覆写数据库中的键值对
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    ......
	    // 移除键的过期时间
    removeExpire(db,key);
    // 发送键修改通知
    signalModifiedKey(db,key);

}

这里实现判断有没有这个key,没有就添加设置,有的话就取覆盖。

1、lookupKeyWrite & lookupKey

robj *lookupKeyWrite(redisDb *db, robj *key) {

    // 删除过期键
    expireIfNeeded(db,key);

    // 查找并返回 key 的值对象
    return lookupKey(db,key);
}
robj *lookupKey(redisDb *db, robj *key) {
    // 查找键空间
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 节点存在
    if (de) {
        // 取出值
        robj *val = dictGetVal(de);
			.........
        // 返回值
        return val;
    } else {
        // 节点不存在
        return NULL;
    }
}

这里就是通过key使用dictFind方法来查找对应的dictEntry,然后通过dictGetVal来获取value

dictGetVal(he)
#define dictGetVal(he) ((he)->v.val)

2、dictFind

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    unsigned int h, idx, table;
    ......
    // 如果条件允许的话,进行单步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算键的哈希值
    h = dictHashKey(d, key);
    // 在字典的哈希表中查找这个键
    // T = O(1)
    for (table = 0; table <= 1; table++) {
        // 计算索引值
        idx = h & d->ht[table].sizemask;
        // 遍历给定索引上的链表的所有节点,查找 key
        he = d->ht[table].table[idx];
        // T = O(1)
        while(he) {

            if (dictCompareKeys(d, key, he->key))
                return he;

            he = he->next;
        }
        // 如果程序遍历完 0 号哈希表,仍然没找到指定的键的节点
        // 那么程序会检查字典是否在进行 rehash ,
        // 然后才决定是直接返回 NULL ,还是继续查找 1 号哈希表
        if (!dictIsRehashing(d)) return NULL;
    }

    // 进行到这里时,说明两个哈希表都没找到
    return NULL;
}

这里的逻辑就是先通过dictIsRehashing(d)判断当前是不是还在进行rehash,如果是的话就通过_dictRehashStep(d)来进行渐进rehash

完成后就通过h找到两张ht表对应槽位的元素,遍历这个链表,找到了就返回完成本次搜索。

3、单步渐进hash

1)、dictIsRehashing(ht)

#define dictIsRehashing(ht) ((ht)->rehashidx != -1)

首先是通过rehashidx != -1判断其是不是在扩容,如果为-1表示其没有扩容,如果为正,表示其在扩容,同时这个值是顺序自增的,没进行一次单步rehash+1。一直到ht[0]used为0,就是rehash完成,都转移到了ht[1]。然后进行rehash

2)、_dictRehashStep(dict *d) & dictRehash(dict *d, int n)

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {
		........
    // 进行 N 步迁移
    // T = O(N)
    while(n--) {
        dictEntry *de, *nextde;

        /* Check if we already rehashed the whole table... */
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
            // 释放 0 号哈希表
            zfree(d->ht[0].table);
            // 将原来的 1 号哈希表设置为新的 0 号哈希表
            d->ht[0] = d->ht[1];
            // 重置旧的 1 号哈希表
            _dictReset(&d->ht[1]);
            // 关闭 rehash 标识
            d->rehashidx = -1;
            // 返回 0 ,向调用者表示 rehash 已经完成
            return 0;
        }
        // 略过数组中为空的索引,找到下一个非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

        // 指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {
            unsigned int h;
            // 保存下个节点的指针
            nextde = de->next;
            /* Get the index in the new hash table */
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 插入节点到新哈希表
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            // 更新计数器
            d->ht[0].used--;
            d->ht[1].used++;
            // 继续处理下个节点
            de = nextde;
        }
        // 将刚迁移完的哈希表索引的指针设为空
        d->ht[0].table[d->rehashidx] = NULL;
        // 更新 rehash 索引
        d->rehashidx++;
    }
    return 1;
}

这个就是rehash的代码,还是很清晰的:

这个入参n,就是步长,就是一次转移几个hash表的槽位,以及与下面的d->rehashidx++是对应的。所谓渐进式rehash,就是分多长完成rehash数据转移,同时在转移过程中ht[0]ht[1]是同时使用的,如果0表查询不到,就会去1表查。

然后这里实现是判断ht[0].used == 0表正在使用的表示不是为0,如果为0,就表示已经都转移到ht[1],就表示已经完成了整个rehash转移,就将现在的ht[1]设置为ht[0],然后再d->rehashidx = -1;表示已经完成了rehash扩容。

再之后,就是将ht[0]对应槽位的链表(while(de)de = nextde;)都转移到ht[1]中,同时d->ht[0].used--d->ht[1].used++

并且入参n多大,就进行几次while槽位转移。

3)、rehash步长方法的调用时机

那一般时候调用呢?

1、目前我们就是在通过key查到的时候调的:dictFind(dict *d, const void *key)

2、在进行字典表数据的添加的时候:dictAddRaw(dict *d, void *key)

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    // 如果条件允许的话,进行单步 rehash
    // T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);
		........

3、在随机获取key的时候:dictGetRandomKey(dict *d)

dictEntry *dictGetRandomKey(dict *d)
{
    dictEntry *he, *orighe;
    unsigned int h;
    int listlen, listele;

    // 字典为空
    if (dictSize(d) == 0) return NULL;

    // 进行单步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

4、在进行删除的时候:dictGenericDelete(dict *d, const void *key, int nofree)

static int dictGenericDelete(dict *d, const void *key, int nofree)
{
    unsigned int h, idx;
    dictEntry *he, *prevHe;
    int table;

    // 字典(的哈希表)为空
    if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */

    // 进行单步 rehash ,T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

5、还有一个就是在后台处理的:dictRehashMilliseconds(dict *d, int ms)

int dictRehashMilliseconds(dict *d, int ms) {
    // 记录开始时间
    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        // 如果时间已过,跳出
        if (timeInMilliseconds()-start > ms) break;
    }

    return rehashes;
}

可以看到这个方法的步长是100。不像前面的_dictRehashStep默认为1

4、dbAdd(redisDb *db, robj *key, robj *val)

这个就是添加键值到db中:

void dbAdd(redisDb *db, robj *key, robj *val) {

    // 复制键名
    sds copy = sdsdup(key->ptr);

    // 尝试添加键值对
    int retval = dictAdd(db->dict, copy, val);
	......
 }
int dictAdd(dict *d, void *key, void *val)
{
    // 尝试添加键到字典,并返回包含了这个键的新哈希节点
    // T = O(N)
    dictEntry *entry = dictAddRaw(d,key);

    // 键已存在,添加失败
    if (!entry) return DICT_ERR;

    // 键不存在,设置节点的值
    // T = O(1)
    dictSetVal(d, entry, val);

    // 添加成功
    return DICT_OK;
}

这个就是添加的逻辑,不过这个dictAddRaw(d,key)方法我们通过,这里就不再赘述了。我们这里可以关注到另一种数据结构,就是sds。这个是redis设计用来取代字符数组,可以看到目前我们的key就是用这个结构表示的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZoVvMCP-1626492884755)(F:\MarkDown-File\中间件源码\Redis源码解读(1)-基本的数据结构.assets\image-20210717093615247.png)]

三、redis中的一些其他数据结构

/* Object types */
// 对象类型
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
// 对象编码
#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

redis有上面的五种面向用户的类型,当其内部是用的REDIS_ENCODING_XXX这些类型处理的。

1、sdshdr(sds)

struct sdshdr {

    int len;

    int free;

    char buf[];
};

这个sdshdr是redis设计用来存放字符串的,这个结构其使用buf[]来存放字符数组、len表示占用的空间、free表示还有多少没有使用。

redis源码分析系列 redis源码解读_redis_10

目前我们的keyt5,然后当前sdshdr的长度为2,因为都占满了,所以free0

redis源码分析系列 redis源码解读_redis_11

2、zskiplist 跳跃表

typedef struct zskiplistNode {

    // 成员对象
    robj *obj;

    // 分值
    double score;

    // 后退指针
    struct zskiplistNode *backward;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

/*
 * 跳跃表
 */
typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist;

这个就是跳跃表的结构。首先是zskiplistNode,也就是跳跃表的节点结构体:

1)、zskiplistNode

score表示分值(跳跃表是用来给zset类型使用的,但要注意,zset类型不一定使用这个skiplist,数量少的时候可能是使用的压缩列表)。

robj *obj,表示key对应的值描叙,redis中,我们前面提到的dictEntry其的值(不管是用的哪种数据类型),都是会构建一个robj

typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

然后这里的type:4就是表示的对象类型,就是我们前面提到的REDIS_ENCODING_XXX这些,然后里面达到一定的条件就会进行一些转换。*ptr表示指向真正的类型。

然后这个结构体主要是有level[],这个也就是具体的跳远表的内容。主要有两个参数forward表示指向后面的指针、span表示向后面跳了多少个节点,也就是跨度为多少。

redis源码分析系列 redis源码解读_redis数据结构_12

上面这个就是跳跃表的简单模型(这里只是为了说明的一个简单模型),这个例如说,当我们要查找5节点,我们就不用从首节点开始往下遍历,注解就知道第一层然后直接跳到4节点,往下。同样要找10的话,也可以直接找到第二层注解到8节点。感觉有点树结构的味道了。

2)、zskiplist

zskiplist就是描叙跳跃表,保持头结点、尾结点、保持的元素个数等。

3、encoding编码类型说明

例如我们现在使用hset hs1 k1 v1来设置,在c->cmd->proc(c);这里就会调用hsetCommand(redisClient *c)、(t_hash.c文件),而我们前面的set t5 v4是调用的setCommand(redisClient *c)、(t_string.c文件)。

void hsetCommand(redisClient *c) {
    int update;
    robj *o;

    // 取出或新创建哈希对象
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;

    // 如果需要的话,转换哈希对象的编码
    hashTypeTryConversion(o,c->argv,2,3);

    // 编码 field 和 value 对象以节约空间
    hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);

    // 设置 field 和 value 到 hash
    update = hashTypeSet(o,c->argv[2],c->argv[3]);
	..........
    // 将服务器设为脏
    server.dirty++;
}

然后这里的hashTypeLookupWriteOrCreate方法就是创建robj对象:

robj *hashTypeLookupWriteOrCreate(redisClient *c, robj *key) {

    robj *o = lookupKeyWrite(c->db,key);

    // 对象不存在,创建新的
    if (o == NULL) {
        o = createHashObject();
        dbAdd(c->db,key,o);
    // 对象存在,检查类型
    }..........
    // 返回对象
    return o;
}
robj *createHashObject(void) {

    unsigned char *zl = ziplistNew();
    robj *o = createObject(REDIS_HASH, zl);
    o->encoding = REDIS_ENCODING_ZIPLIST;
    return o;
}

然后我们可以看到现在其的初始化也是设置的o->encodingREDIS_ENCODING_ZIPLIST

然后下面的hashTypeTryConversion我们可以看到其达到一定的条件其就会转换为REDIS_ENCODING_HT也就是hash表。

void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;

    // 如果对象不是 ziplist 编码,那么直接返回
    if (o->encoding != REDIS_ENCODING_ZIPLIST) return;

    // 检查所有输入对象,看它们的字符串值是否超过了指定长度
    for (i = start; i <= end; i++) {
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
        {
            // 将对象的编码转换成 REDIS_ENCODING_HT
            hashTypeConvert(o, REDIS_ENCODING_HT);
            break;
        }
    }
}

4、压缩列表

/* 
空白 ziplist 示例图

area        |<---- ziplist header ---->|<-- end -->|

size          4 bytes   4 bytes 2 bytes  1 byte
            +---------+--------+-------+-----------+
component   | zlbytes | zltail | zllen | zlend     |
            |         |        |       |           |
value       |  1011   |  1010  |   0   | 1111 1111 |
            +---------+--------+-------+-----------+
                                       ^
                                       |
                               ZIPLIST_ENTRY_HEAD
                                       &
address                        ZIPLIST_ENTRY_TAIL
                                       &
                               ZIPLIST_ENTRY_END

非空 ziplist 示例图

area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL
typedef struct zlentry {

    // prevrawlen :前置节点的长度
    // prevrawlensize :编码 prevrawlen 所需的字节大小
    unsigned int prevrawlensize, prevrawlen;

    // len :当前节点值的长度
    // lensize :编码 len 所需的字节大小
    unsigned int lensize, len;

    // 当前节点 header 的大小
    // 等于 prevrawlensize + lensize
    unsigned int headersize;

    // 当前节点值所使用的编码类型
    unsigned char encoding;

    // 指向当前节点的指针
    unsigned char *p;

} zlentry;

这个就是压缩列表的具体节点entry1entry2这些结构体,我们可以其不同于一般的链表,是通过前后指针,压缩列表是连续的数组结构。当他与一般的数组与不同例如int[]这种,这种只能存储一种确定的类型,二压缩列表是能存储多种的。

所以由这个我们就能找到,如果要存储不同的长度内容话,其首先要记录下自己所占的长度lensize, len

然后这里也有encoding,这个我们前面提过,就是达到一定条件可能就会解析数据结构的转换。

然后我们要遍历的话,我们由于不是链表有前后指针,所以我们就需要知道前面节点的长度才能获取prevrawlensize, prevrawlen。以上就是这个压缩列表的一些基本内容了。