redis系列,给你看最完整的字典讲解


前言

标题字越少,分量越不一样,上篇文章我们已经介绍到了,redis是如何从网络io读取数据到执行命令的过程,今天我们来讲redis第二大基础数据字典


为什么要讲字典?

大家都知道redis 是一个key value, 而大家用得最多的就是set和get操作,那么一个set 操作究竟要经历些什么了,我们来看看。
如果不知道为什么会执行命令请回过头来看这篇文章
redis系列,redis是如何执行命令(一).

struct redisCommand redisCommandTable[] = {

    /* Note that we can't flag set as fast, since it may perform an
     * implicit DEL of a large key. */
    {"set",setCommand,-3,
     "write use-memory @string",
     0,NULL,1,1,1,0,0,0},

从上篇文章我们知道,redis 经过一些前置判断后,最终会调用setCommand.
上文注释说到,set 并不是一个快速的命令,因为它可能引发删除一个大键,话不多说
我们直接看代码

首先我们来看下整个db的结构
server.h

/* 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 *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
    //用bl pop 命令会涉及到
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    //跟watch 命令相关的key
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    //database id
    int id;                     /* Database ID */
    //这个在过期那章讲过,会采样统计进来
    //平均过期时间
    long long avg_ttl;          /* Average TTL, just for stats */
    //这个之前在过期那章也有讲过,
    //当一个expire cycle没有处理完的时候
    //会从这个游标位置继续处理
    //diction 包含一个entry数组
    //entry[expires_cursor]
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    //内存碎片整理相关后续再回过头来看
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

下面是db的初始化
server.c

 //我们的db的初始化
    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        server.db[j].expires_cursor = 0;
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
        server.db[j].defrag_later = listCreate();
        listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
    }

set命令的入口

server.c

/* SET key value [NX] [XX] [KEEPTTL] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    //默认单位秒
    int unit = UNIT_SECONDS;

    int flags = OBJ_SET_NO_FLAGS;
    //从上篇文章我们知道,argc 是参数个数
    // 前两个参数set [key]
    for (j = 3; j < c->argc; j++) {
        //argv 是robj结构, ptr 指向的是一个char[]
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        //设置状态位
        //nx 和xx 是互斥的,且这里看到是忽略大小写的
        //nx not exist的意思, 表示键不存在则写入
        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
            !(flags & OBJ_SET_XX))
        {
            flags |= OBJ_SET_NX;
        } 
        //xx的逻辑
        //xx 就是表示键存在则写入覆盖
        else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_NX))
        {
            flags |= OBJ_SET_XX;
        } 
        //这个意思保持原来键的过期时间
        //strcasecmp的是忽略大小写比较
        else if (!strcasecmp(c->argv[j]->ptr,"KEEPTTL") &&
                   !(flags & OBJ_SET_EX) && !(flags & OBJ_SET_PX))
        {
            flags |= OBJ_SET_KEEPTTL;
        } 
        //ex 是以秒为单位
        else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_KEEPTTL) &&
                   !(flags & OBJ_SET_PX) && next)
        {
            flags |= OBJ_SET_EX;
            unit = UNIT_SECONDS;
            expire = next;
            //这个j++ 很灵性跳过了,参数值直接指向了下一项参数
            j++;
        } 
        //px 是以毫秒为单位,与keepttl 不能同时存在
        else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_KEEPTTL) &&
                   !(flags & OBJ_SET_EX) && next)
        {
            flags |= OBJ_SET_PX;
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            //走到这里说明了不可解析的参数
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    //这里是redis key, 做压缩处理,
    // 这里埋个坑讲压缩string的结构再回过头来看
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

void setGenericCommand(client *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 */
    //expire 不为空的时候
    if (expire) {
        //  因为expire 是可以转换成整型,如果转换错误则直接return
        //  然后赋值给milliseconds
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        // expire 不能为负数    
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        // 如果是单位seconds 则
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }
    // 如果设置了nx 或者xx 那么会先去查询一次key,
    // 且key 如果已经过期的情况下,xx是会失败的
    // 反之key存在的情况下,nx会失败
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
        return;
    }
    // 这里开始是真正放入db里面的情况。
    // c-db 会在createClient 默认情况为db0,
    // 可以用select 来修改db
    genericSetKey(c,c->db,key,val,flags & OBJ_SET_KEEPTTL,1);
    //每次修改都加1用于rdb 来计数
    server.dirty++;
    //如果过期时间不为空,则重新设置过期时间
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    // 这个是跟reids-module相关的事件通知
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
    //过期键的相关通知
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);
    addReply(c, ok_reply ? ok_reply : shared.ok);
}



void genericSetKey(client *c, redisDb *db, robj *key, robj *val, int keepttl, int signal) {
    //look up key write除了会找这个key 是否在key dictionary
    if (lookupKeyWrite(db,key) == NULL) {
        //db add 
        dbAdd(db,key,val);
    } else {
        //以前存在就覆盖
        dbOverwrite(db,key,val);
    }
    // val的引用+1
    incrRefCount(val);
    // 如果keepttl 为空,则去除过期时间
    //以新的设置为主,这个会与ex px 关键字互斥
    // 可以看前面代码就知道
    if (!keepttl) removeExpire(db,key);
    //  这里跟watched 命令相关,
    //  还有跟客户端缓存相关的逻辑
    if (signal) signalModifiedKey(c,db,key);
}


/* Add the key to the DB. It's up to the caller to increment the reference
 * counter of the value if needed.
 *
 * The program is aborted if the key already exists. */
void dbAdd(redisDb *db, robj *key, robj *val) {
    // 分配一个新的空间,因为旧的空间会被回收
    sds copy = sdsdup(key->ptr);
    // dic add 一个字典增加一个数据,我们在下面文章开始分析
    int retval = dictAdd(db->dict, copy, val);

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET ||
        val->type == OBJ_STREAM)
        //这部分跟后续的数据类型有关,后续回头讲
        signalKeyAsReady(db, key);
        //跟cluster 相关的
    if (server.cluster_enabled) slotToKeyAdd(key->ptr);
}

/* Overwrite an existing key with a new value. Incrementing the reference
 * count of the new value is up to the caller.
 * This function does not modify the expire time of the existing key.
 *
 * The program is aborted if the key was not already present. */
void dbOverwrite(redisDb *db, robj *key, robj *val) {
    // 找到对应的entry
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 断言打印堆栈
    serverAssertWithInfo(NULL,key,de != NULL);
    //这里是值传递哦,意思复制了一份de,
    // 不明白要百度下引用和值传递的不同
    dictEntry auxentry = *de;
    robj *old = dictGetVal(de);
    //执行lfu 政策的时候要把之前的的信息传递过来
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        val->lru = old->lru;
    }
    //设置新的值
    dictSetVal(db->dict, de, val);
    //是否设置了惰性删除空间
    if (server.lazyfree_lazy_server_del) {
        //异步回收旧值空间
        freeObjAsync(old);
        //这里设置为null ,
        // 则下面的方法不会再次执行freeval
        dictSetVal(db->dict, &auxentry, NULL);
    }
    //回收val
    dictFreeVal(db->dict, &auxentry);
}

从上面的代码可以看到,redis set命令经过一系列的操作会将数据存入dict 这个struct 里面。所以对于redis 而言其本身就是一个大的字典结构,只是在这个字典结构又扩展其它的数据结构。

为什么不是hashtable 而是dict

因为dict 的概念相对于hashtable而言更为广阔,因为实现字典的方式并不一定是hash,也有可能是数组。

数组实现字典

比如我有一个需求想知道梁山108好汉,每个编号对应的武将那么,因为每个id都是唯一的情况下,我完全可以通过数组索引的方式实现这个的key-value需求,而且时间复杂度为o(1)

前缀树实现字典

比如我们key都是由abcd…组成,且key的长度可控的情况,对于这种需求我们也同样可以用前缀树来完成key-value的需求,而且时间复杂度也是为o(1)

public class TrieTree {
    
    List<String> value;
    
    TrieTree[] trieTrees;
    
}

以上举出了两个例子来说明,字典不一定需要hash table 来实现,当然在redis里面字典主要结构还是hashtable, 下面我们来讲什么是hashtable

hashtable

hashtable 的主体结构其实也是一个数组,
实现一个hashtable主要有3要素,hash算法,如何解决hash冲突,扩容。

hash 算法

hash算法的目的主要让数据散列起来,这是什么意思了,我们开始说了我们可以用数组去表示梁山108好汉,现在我们问题开始升级了,现在出现了多重宇宙,如果每次多重宇宙都是一起来还好,但现实情况总是随机的,可能来了10000次宋江,而只来了1次林冲,那么如果我们用一个二维数组去表示这些人的话,就出现问题,每次找宋江,最坏情况下我需要遍历10000次。且其它空间也无法得到利用,具体可见下图

在这里插入图片描述
那么好的解决方案就是把(宋江)分散开来,那么就需要用到hash算法,常用的hash算法有哪些了,乘法hash,加法hash,混合hash 等等,hash算法必须有两个特点
一, 就是让原本具有顺序性的字符,数字变得更为随机,这样上图那些没有分配的空位就能分配到。再具体代码部分我们会继续去说。
二,计算不能太复杂,比如一些复杂的加密算法就不太适合。因为会耗费大量的计算时间。这里说的太复杂可以理解为cpu 平均计算时间。

hash冲突

hash算法是服务分配我们上图的空位,让我们的空位尽量的均匀,所以即使hash算法足够随机最后也是要按需分配,最后的栏位分配位置等于hashcode对这个数组长度取模。我们对分配同一个bucket上面的行为,称为hash冲突,因为size不可能是一个非常大的数,所以冲突也是常态,产生冲突的解决方案主要有三种
1, 拉链法,redis里面也是使用的拉链法,具体代码我们在下面分析。
大体的方式就是发生冲突时,利用链表的指向性,生成新节点,
拉链法的结构如下:
在这里插入图片描述

2, 开放地址法。
开放地址法,数据结构就是一个普通的数组。解决冲突的方式最常用的就是线性探测
在这里插入图片描述

关于开放地址法还有二次探测和伪随机探测等等,有其它文章已经有详细讨论这边不继续深入了。

3, rehash法。
使用场景当数组比较稀疏的时候可以使用,就是当冲突的时候就继续hash,直到不冲突才插入。可以想象,在比较稠密的数组里面,可以想象冲突几率会很大,会计算多次。对于插入和查询都是比较大的开销。
总结:
以上三种方式,拉链法是优点是比较多的,好扩容,删除,修改都比较友好,但是会有额外的指针空间消耗,基本场景都适合。

开放地址法适合元素较小的场景,比如java 里面的threadlocal.
缺点是查询效率最坏情况下可能直接到o(n)(如上图5和6连起来,当下次遇到需要插入3的位置的数,直到遍历到6才知道需要扩容,当然扩容策略可以调整,但是这种最情况下在内存节省的情况下无法避免) ,扩容时,内存基本不可复用,需要重新rehash到新的数组。优点是就是一个数组,没有额外的内存花销。

rehash, 就不用多说了,如果rehash的位置都是冲突的情况下,需要进行多次计算。且扩容方面等同开放地址法。

扩容

hash 扩容我们主要以拉链法的背景下来讨论,其余的扩容方式可能会另外写文章来讨论,首先要知道为什么要扩容,扩容的原因是我们的元素存到太过稠密,而导致无可避免的链表长度过长。比如上图举例的宋江的情况,比如我们要存120个元素,而拉链表的竖向长度只有4,那么无可避免会导致横向过长,导致存取速度越来越慢,
避免这种情况,所以我们要把竖向长度变长。但是扩容也不是随便扩容,我们的目的是减少横向的长度,所以我们不能因为扩容又导致横向长度变得更长。所以竖向的长度必须是整数的次方。
拿2的次方举例
在这里插入图片描述

可以看到扩容后的元素要么在原来不动,要么跑去了高位,因为2的次数,数的因数组成就是2,根据除法的原理,我们可以把5/4 分解为5/2/2 ,所以对于任何数的2^n 的余数= 2^n+ 2的余数,同理也可以得3的n次幂同样的道理,这样就能保证,扩容的时候以前在不同bucket里面的数,扩容后不会进入到同一个bucket里面去,从而达到扩容的目的

为什么很多开源软件都是用的2的n次方来扩容了,原因是取模操作相对来说比较复杂,取模在计算机里面是一个复杂操作,可以自行百度下,这里不过多去解读,所以对于hash出来的数都是通过一个sizeMask做and操作,sizeMask=size-1,因为是2的次方都是00001000这种,减去1就变成了00000111,与其它数做and操作能达到取模的效果。而且位运算速度显然是要比取模速度快很多
另外以2倍的扩容增长程度也很快了,用3或者4会增加扩容和收容的时间成本,所以选用2倍扩容是一个合适的方案。

对于redis 而言扩容和收容都是一个需要思考比较深的事,为什么了因为redis内部是一个单线程处理,如果扩容和收容花费过多的时间,那么将会给redis 造成大的阻塞,所以redis 采用了一种渐进式的rehash方式来处理

普及完字典的知识后我们开始上讲代码

dict 代码

首先我们看一下redis 定义的结构

//字典的主结构
typedef struct dict {
    //字典类型,来适配各种需求
    dictType *type;
    // privdata 可以用于指向一个方法
    // 也可以是一个
    void *privdata;
    //两个table,主要负责rehash的时候
    //字典也能够正常使用
    dictht ht[2];
    //rehasdidx 可以表示rehash的进度
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    //统计有多少个iterator运行中
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
// 这个hash table的结构,每个字典有两个hashtable,用于rehash
typedef struct dictht {
    // 相当于一个entry的指针数组
    dictEntry **table;
    //entry的指针数组的长度
    unsigned long size;
    //sizemask = size-1,用于做and操作
    //来分配key对应的entry
    unsigned long sizemask;
    //目前table里面存在元素的个数
    unsigned long used;
} dictht;

// dictype 主要定义了一些方法,帮助不同类型
// 的参数可以实现差异化的策略
typedef struct dictType {
    // 定义hash 算法,用得比较多hash算法
    // siphash,这个主要是字符串的hash
    uint64_t (*hashFunction)(const void *key);
    // 键重复时执行的方法
    void *(*keyDup)(void *privdata, const void *key);
    // value重复时执行的方法
    void *(*valDup)(void *privdata, const void *obj);
    // key 的比较的方法
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // key 销毁调用
    void (*keyDestructor)(void *privdata, void *key);
    // value 销毁调用
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

typedef struct dictEntry {
    //定义一个指针,
    //那么key可以是一个整型也可以是一个string
    void *key;
    //定一个union 可以即为String,也可以是其它
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指针指向横向的下一个节点
    struct dictEntry *next;
} dictEntry;

具体的结构如图:
在这里插入图片描述

dict的初始化:

//初始化字典
dict *dictCreate(dictType *type,
        void *privDataPtr)
{   
    //给字典分配空间
    dict *d = zmalloc(sizeof(*d));
    //初始化参数
    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    //初始化hashtable
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    //设置类型
    d->type = type;
    //设置私有指针,但是没有模块用到了
    //可能后续增加一些扩展性
    //回调函数等等
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

/* Reset a hash table already initialized with ht_init().
 * NOTE: This function should only be called by ht_destroy(). */
static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

可以看到dict 初始化的时候,只是对dict整体结构进行了初始化,而dict里面的hashtable并没有急着分配空间

dict的put方法

/* Add an element to the target hash table */
//增加元素到字典
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    //将val 设置到entry
    dictSetVal(d, entry, val);
    return DICT_OK;
}

/* Low level add or find:
 * This function adds the entry but instead of setting a value returns the
 * dictEntry structure to the user, that will make sure to fill the value
 * field as he wishes.
 *
 * This function is also directly exposed to the user API to be called
 * mainly in order to store non-pointers inside the hash value, example:
 *
 * entry = dictAddRaw(dict,mykey,NULL);
 * if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
 *
 * Return values:
 *
 * If key already exists NULL is returned, and "*existing" is populated
 * with the existing entry if existing is not NULL.
 *
 * If key was added, the hash entry is returned to be manipulated by the caller.
 */
//这是一个low level的api ,当add的时候existing是为空的
//执行覆盖的操作的时候,要先找到对应的entry,然后执行覆盖
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
    //判断是否在rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    //返回entry数组对应到的index的位置
    //dictHashKey是key的hash算法
    //-1的时候表示有相同的key存在
    //-1 不是hash冲突请注意
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    //如果正在rehash ,则为ht[1]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    //分配空间
    entry = zmalloc(sizeof(*entry));
    //将新的entry,做为头节点指向以前的entry
    entry->next = ht->table[index];
    // 让table[index] 指向新插入的节点
    ht->table[index] = entry;
    // 使用数+1
    ht->used++;

    /* Set the hash entry fields. */
    //给key 赋值
    dictSetKey(d, entry, key);
    return entry;
}

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    //判断是否需要扩容,要记得初始的时候为0
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        //跟sizemask 做and 操作
        //idx 元素所属entry数组的下标
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        //通过idx 找到entry
        he = d->ht[table].table[idx];
        //如果he 不为空则表示出现hash冲突,则需要
        while(he) {
            // 这里是比较key相等,如果有存在的key则返回-1.
            // 然后将让existing 指向he,
            // 等待做下一步操作
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        // 可以看到这里如果是正在做rehash
        // 则返回的是ht[1]的idx
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}



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

put 的操作基本都是key hash->取模->判断空间是否充足->判断key是否已经存在->设值key->设值val。
跟常规hashtable差不多,唯一不同的是设置的会判断是否正在rehash,如果在rehash 就会设值到ht[1]

我们再来看dict的扩容

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    //判断是否正在rehash
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    //初始化完后第一次插入元素会走到这里
    //初始化的第一次插入元素会走到这里
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    //dict_can_resize 是一个全局设置,
    //当有copy on write事件发生会禁止expand
    //当hash表的元素个数大于entry数组长度的时候(前提)
    //且能够resize 判断为真
    //或者已经超过必须扩容的阈值,默认是元素个数是entry数组
    //长度的5倍
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {   
        //扩容长度
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

* Expand or create the hash table */
/**
 * 这个方法仅仅用于分配空间,赋值操作
 * 如果dic本身已经有元素是不会
 */  
int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    //判断hash状态,和size是否合法
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    //size 必须是2的次方
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
    //resize 和现在size相同
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    //开始初始化size
    n.size = realsize;
    //sizemask 用于做and位运算,等同取模操作
    n.sizemask = realsize-1;
    //分配空间等于resize*dicEntry的空间
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    //如果是初始化,则仅仅对h[0]赋值
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    // 赋值给ht[1] ,然后dic rehash状态改变
    // 不遍历所有元素做rehash操作
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

/* Our hash table capability is a power of two */
//保证size 是2的次方
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}


可以看到dict的扩容只是空间的扩容,并没有对key做rehash

渐进式rehash

/* This function performs just a step of rehashing, and only if there are
 * no safe iterators bound to our hash table. When we have iterators in the
 * middle of a rehashing we can't mess with the two hash tables otherwise
 * some element can be missed or duplicated.
 *
 * This function is called by common lookup or update operations in the
 * dictionary so that the hash table automatically migrates from H1 to H2
 * while it is actively used. */
//如果有遍历器则不做rehash
//可以想象下,遍历器主要扫描
//entry中的其中一个
//这样就会导致扫描出来的数据不准确
//具体iterator 我们在scan 
// 这一章节分析
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    //只有在rehash才会进入这里
    if (!dictIsRehashing(d)) return 0;
    //开始遍历旧hashtable里面的元素
    //used要大于0
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        //rehashidx 可以看做rehash的进度
        //这里就是开始遍历到不为空的节点
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            //这里就是控制遍历的次数,跟传进来n有关
            if (--empty_visits == 0) return 1;
        }
        
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        //链表典型的遍历方式
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            //需要对新的hashtable 里面重新取模
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            //跟插入流程一样,移入的元素放入头节点
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            //used 开始--和++
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        //完成后将旧节点设置为null
        d->ht[0].table[d->rehashidx] = NULL;
        //rehashid 更新,
        //下次就会从新的层次继续
        //rehash
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    //当旧的链表全部已经处理完
    if (d->ht[0].used == 0) {
        //释放内存
        zfree(d->ht[0].table);
        //ht[0]重新做为主存的hashtable
        d->ht[0] = d->ht[1];
        //重制ht[1]
        _dictReset(&d->ht[1]);
        //rehashidx 变为-1, 即当前状态为正常态
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

可以看到redis 采用这种空间换时间的方式采用渐进式hash的方式,redis为什么要这么做了因为redis是单线程操作,如果一次性rehash的话,当整个hash的元素比较多的时候会对其它客户端命令造成较久的等待,所以redis采用这种方式将rehash的步骤分散到每一次的set或者get操作。

总结:

本节文章通过set命令作为入口,讲到了字典在redis 里面的应用,然后讲解了字典和hashtable的关系,详细的描述了redis是如何来设计这个字典的结构,但是对hash算法还没有提及,但是redis 字典里面用得比较多的hash算法是siphash,其主要也是根据key的特性来设计不同的hash算法,宗旨就是让更多位参与运算,减少hash碰撞。这个也是有一些业界比较公用的算法,后续我们研究深入后再继续讨论。下节我们会再讲解一些和字典相关的操作如scan ,keys, 希望大家能够多关注。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

偷懒的程序员-小彭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值