Redis的存储结构笔记

hash:给定一个字符串或者其他任意的值X,通过hash函数得到一个散列值;hash表的意思就是建立一个数组,

问题:通过索引(hash值)去读取hash表,hash表会非常大,占用内存非常庞大;通过hash遍历hash表,k-v数量多,查找性能会降到O(N),时间性能也很低。

redis的hash表:

typedef struct dictht {

 dictEntry **table;// 一个数组,数组中每一个元素都有一个指向dictEntry的指针,每个dictEntry结构保存着一个键值对。

 unsigned long size; //哈希表的大小,也就是table表的大下。

 unsigned long sizemask; //值总是等于size-1,这个值和hash值一起决定键应该被放在哪个索引上面。

 unsigned long used;//表示 hash表中已有的节点的数量

} dictht;

进行存储的时候  直接使用hash&sizemask就可以得到相应的index,这index值范围0~size-1,也就是0-sizemask;



hash表的节点:

typedef struct dictEntry {

void *key;  // 键 保存键值对中的键,v保存键值对中的值,值可以使一个指针,或者一个整数或者double类型。

 // 值 

 union {

 void *val; 

 uint64_t u64;

 int64_t s64; 

 double d; } v;

 struct dictEntry *next; // 指向下个哈希表节点,形成链表 ,将多个哈希值相同的键值对连接在一起,解决键冲突的问题。

} dictEntry;



redis字典 dict :

typedef struct dict { 

 dictType *type; // 字典类型 指向dictType结构的指针,每个dictType结构保存了一组操作特定类型键值对的函数

 void *privdata;  //保存了需要传给那些类型特定函数的可选参数

 dictht ht[2]; // 数组中每个项都是一个dictht哈希表,字典使用ht[0],ht[1]哈希表在对ht[0]rehash时使用。

 long rehashidx; //rehash的位置,如果值不等于-1 说明正在rehash中。

 unsigned long iterators;  // hash表的迭代器,一般用于rehash和主从复制等等 

} dict;



redis如何解决时间效率和空间效率的?

初始化时,字典的hash表大小是4(sizemask是3),通过hash计算出的hash值可能很大,hash值&sizemask,得以存放在表中。hash表是随着k-v数量的增加而逐步增大的,并不直接以hash值为下标去取值,以index=hash值&sizemask 获取下标,取得对应节点,节点是个链表。

ratio=ht[0].used/ht[0].size;  

当ratio>=1并且没有进行主从复制和持久化 ,进行扩容,主从复制和持久化时 ratio>5 会进行扩容(避免系统负载高);

当ratio<0.1进行缩容。

可以看出,并不是开始申请大量内存,而是渐进式扩容或者缩容。

扩容步骤; 

1为字典ht[1]哈希表分配合适的空间(扩容的大小为大于size的2的n次方。位运算方便。)2将ht[0]中所有键值对rehash到ht[1]。3

当ht[0] 包含的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建一个空白哈希表,为下次rehash准备。

渐进式rehash执行流程源码:

aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)//创建定时器回调,增量处理许多后台定时器的方法:过期键,rehash等

//定时器中断,每秒调用;异步完成许多事情

serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {

//省略其他代码

.......

 /* Handle background operations on Redis databases. */

 redis数据库后台操作。

    databasesCron();

}

databasesCron函数与rehash相关代码,其他代码省略了

void databasesCron(void){

    int dbs_per_call = CRON_DBS_PER_CALL; //16

    .........

     /* Rehash */

        if (server.activerehashing) {

            for (j = 0; j < dbs_per_call; j++) {

                int work_done = incrementallyRehash(rehash_db); //对每个数据库进行渐进式rehash,如果返回1,执行时间到了直接跳出,下次继续进行操作,如果返回0,说明此数据库已经完成,进行下一个数据库继续进行操作。

                if (work_done) {

                    /* If the function did some work, stop here, we'll do more at the next cron loop. */

                    break;

                } else {

                    /* If this db didn't need rehash, we'll try the next one. */

                    rehash_db++;

                    rehash_db %= server.dbnum;

                }

            }

        }

    ..............

}

调用incrementallyRehash函数执行渐进式rehash,看此函数中逻辑代码

int incrementallyRehash(int dbid) {

检测数据库是否进行rehash操作中(rehashidx值是否为-1),没有进行rehash 执行其他代码。

    if (dictIsRehashing(server.db[dbid].dict)) {

        dictRehashMilliseconds(server.db[dbid].dict,1);//进行对dbid索引数据库进行时长为1毫秒的rehash操作,返回-1,下次继续

        return 1; /* already used our millisecond for this loop... */

    }

    ......

    return 0;

}

进入dictRehashMilliseconds函数看实现逻辑。

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */

int dictRehashMilliseconds(dict *d, int ms) {

    long long start = timeInMilliseconds();

    int rehashes = 0;

    while(dictRehash(d,100)) {// 每次循环进行100个桶的rehash操作,直到用时超过1毫秒便返回。

        rehashes += 100;

        if (timeInMilliseconds()-start > ms) break;

    }

    return rehashes;

}

到dictRehash函数这 才是真怎执行rehash操作的函数。

int dictRehash(dict *d, int n) {

    int empty_visits = n*10; /* Max number of empty buckets to visit. */

    if (!dictIsRehashing(d)) return 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);

        while(d->ht[0].table[d->rehashidx] == NULL) {

            d->rehashidx++;

            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     从ht[0]中移动桶中的所有keys到ht[1]中*/

        while(de) {

            uint64_t 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;

        d->rehashidx++;

    }

    /* Check if we already rehashed the whole table...  整个表都已经rehash完成,返回0,used是表中节点个数*/

    if (d->ht[0].used == 0) {

        zfree(d->ht[0].table);

        d->ht[0] = d->ht[1];

        _dictReset(&d->ht[1]);

        d->rehashidx = -1;

        return 0;

    }

    /* More to rehash... */

    return 1;

}

以上就为rehash流程。



rehash时新的数据插入到哪里?

插入数据调用的函数为 dictAdd(),源码如下

/* 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;

    dictSetVal(d, entry, val);

    return DICT_OK;

}

调用dictAddRaw进行插入

dictAddRaw(dict *d, void *key, dictEntry **existing){

ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; //哈希表的选择,如果正在rehash操作,新的数据将会插入到ht[1]中。

}



rehash时查询数据是在哪个表中查的?

查询时候是在两个中都进行查询,源码如下。

dictEntry *dictFind(dict *d, const void *key)

{

    dictEntry *he;

    uint64_t h, idx, table;

    if (dictSize(d) == 0) return NULL; /* dict is empty */

    if (dictIsRehashing(d)) _dictRehashStep(d);

    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) { //两个表循环遍历进行数据查询

        idx = h & d->ht[table].sizemask;

        he = d->ht[table].table[idx];

        while(he) {

            if (key==he->key || dictCompareKeys(d, key, he->key))

                return he;

            he = he->next;

        }

        if (!dictIsRehashing(d)) return NULL;

    }

    return NULL;



缩容按照下面计算:ratio值小于0.1时进行

resize :size>4 && used*100/size < 10;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351