Redis dict(字典)

一 前言

dict 常见称之字典(dictionary)或映射(map) ,其元素以键值对形式存在。是 Redis 最重要、常用的数据结构,可以说 Redis 本质就是一个 dict。

Redis 是一个内存型数据库,在 server.h 不难发现这样的定义:

typedef struct redisDb {
    dict *dict;                 /* 使用 dict 来存储键值对 */
    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 */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    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;

dict 是为了解决算法中的查找问题而出现的,在无冲突下理论上能达到 O(1) 查找效率。Dict 本质其实就是一个 hashtable,所以也要面临 hash冲突、扩容缩容等问题。让我们带着这些问题继续看本文的正文部分。

二 结构分析

2.1 dict

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 默认为 -1。当在进行 rehash 时候改值会累增
    unsigned long iterators; /* 正在运行的迭代器数量 */
} dict; 

1. dictType *type
  指向 dictType 结构的指针, 可以以 OOP 中 interface 的方式理解这个结构,它是一个封装了对不同数据类型的操作接口,以支持不同的数据类型。由继承的数据类型实现具体逻辑,这一块读者可以自行查阅。
2. void *privdata
  私有数据指针(privdata),由调用者在创建 dict 的时候赋值。和 dictType 一起实现多态字典。
3. dictht ht[2]
   代表有01两个哈希表,为了实现渐进式扩容而设计的机制。下文会解释。
4. long rehashidx
  长整形数字,默认为 -1。当在进行 rehash 时候改值会累增。下文会解释。

2.2 dictht(dict hash table)

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

1. dictEntry **table
   这就是我们常说的哈希桶(bucket),哈希表节点指针数组。
2. unsigned long size
   指针数组的大小,redis 很多数据结构中经常出现这种空间换时间的设计。
3. unsigned long sizemask
   指针数组的长度掩码,用于计算 hash 值。大小等于(size-1),每个 key 先经过 hashFunction 计算得到一个哈希值,然后计算(哈希值 & sizemask)得到在table上的位置。相当于计算取余(哈希值 % size)4. unsigned long used
   已存储的数据个数,该值也用于参与计算扩容时新哈希表的大小。used / size = 装载因子,这个比值越大哈希值冲突概率越高。

2.3 dictEntry 哈希表节点

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

1. void *key,键。
2. union 结构体,值。
3. struct dictEntry *next,哈希冲突时开链(开链法(拉链法)),单链表的next指针。

2.4 小结

上面分节描述了 dict 的结构构成,下面用一张图来总结下整个结构。
在这里插入图片描述

三 操作API

这一小节只介绍创建字典和添加元素两个接口,其他的可以看 dict.h 头文件中实现。

3.1 创建字典

1. dictCreate 方法作为创建字段的入口,逻辑较为简单清晰。调用 zmalloc 申请内存以后然后进行初始化。
2. 
dict *dictCreate(dictType *type, void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);
    return d;
}

2. 初始化字典,两个哈希表都是在此处进行初始化。

int _dictInit(dict *d, dictType *type, void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

3.2. 新增操作

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

// 可以通过设置 existing 控制 value 是否可被复写,dictReplace 接口有实现。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

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

    1. 获取新元素的索引,如果元素已经存在 return null
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    2. dictIsRehashing 判断当前字典是否在进行 rehash 操作,也就是 dict 结构中 rehashidx 不为 -1 时。
    若在进行 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++;

    dictSetKey(d, entry, key);
    return entry;
}

四 rehash

上文说过 Dict 本质其实就是一个 hashtable,所以也要面临 hash冲突、扩容缩容等问题。

redis 的哈希表冲突采用的是拉链法方式进行处理,若连表过长势必会影响操作速度。作为一个以速度著称的缓存中间件来说是不可接受的,所说需要将链表进行散列开了。

所以需要扩大哈希槽范围,再者当前哈希表大小不能容纳新的元素。这些情况都需要进行扩容操作。

在 Redis 中哈希表的扩容称之为 渐进式 扩容,意思就是扩容不是一蹴而就,而是循序渐进的。如果直接申请新的内存,将原有的数据直接拷贝过去这势必会需要较大的时间(对于缓存服务来说)。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; 
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        
        assert(d->ht[0].size > (unsigned long)d->rehashidx);

        1. 设置进行 rehash 状态。
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];

        while(de) {
            uint64_t h;

            nextde = de->next;
	        
	        2. 将 ht[0] 哈希表中下标为 rehashidx 的元素剪切到 ht[1] 哈希表。
            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++;
    }
	
	// 完成迁移后,需要对下一次 rehash 做准备。
    if (d->ht[0].used == 0) {
	    1. 释放 0 下标哈希表内存。
        zfree(d->ht[0].table);

        2.0 下标哈希表指向下标 1 哈希表。
        d->ht[0] = d->ht[1];
        
        3. 销毁下标 1 哈希表。
        _dictReset(&d->ht[1]);

        4. 设置未 rehash 状态
        d->rehashidx = -1;
        return 0;
    }

    // 进行迁移中
    return 1;
}

大致的流程如上,看到这里有同学会问:怎么确保完全将 ht[0] 哈希表元素全部复制到新的 ht[1] 哈希表中?继续看 rehash 的过程。

  1. 为ht[1]分配空间,大小计算方式见下面一段。
  2. 将 dict 结构体中 rehashidx 设为0,表示 rehash 正式开始。
  3. 在进行 rehash 期间,每次对该字典进行操作的同时会对 rehashidx 进行累加操作并将 ht[0] 中下标为 rehashidx 的元素剪切到 ht[1]。 当然了新增操作是直接写到 ht[1] 中,可以看上面 dictAdd 接口。
  4. rehash 完成后,将 rehashidx 重新置为 -1、切换 ht[0] 和 ht[1] 指向、重置 ht[1] 为下一次 rehash 准备。

rehash 是一个双向操作,除了扩容也可以缩容。新的哈希表大小计算方式:

  1. 扩容 ht[1] 大小为第一个大于等于 ht[0].used * 2 *2 ^{n} 的整数。
  2. 缩容 ht[1] 大小为第一个大于等于 ht[0].used * 2^{n} 的整数。

五 总结

本文介绍了 redis 中 dict 数据结构,介绍了实现原理和面临问题。当然还有部分细节没有提到,在本文最后做一个总结列出。

  1. 负载因子 = 哈希表当前保存节点数 / 哈希表大小。
  2. 进行 rehash 触发时机,1. 负载因子大于等于 1 时进行扩容操作(当前未执行 BGSAVE 或 BGREWRITEAOF);2. 负载因子大于等于 5 时强制进行扩容操作。
  3. 负载因子小于 0.1 时进行缩容操作。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

mooddance

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

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

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

打赏作者

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

抵扣说明:

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

余额充值