Redis数据结构之——hash

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis数据结构之——hash

写在前面

以下内容是基于Redis 6.2.6 版本整理总结

一、Redis 数据结构hash的编码格式

Redis中hash数据类型使用了两种编码格式:ziplist(压缩列表)、hashtable(哈希表)

在redis.conf配置文件中,有以下两个参数,意思为:当节点数量小于512并且字符串的长度小于等于64时,会使用ziplist编码。

hash-max-ziplist-entries 512    
hash-max-ziplist-value 64  

二、压缩链表(ziplist)

ziplist 我们整理在下一篇文章。

三、哈希表(hashtable)

Redis中的字典(dict)使用哈希表作为的底层实现,一个哈希表里可以有多个哈希表的节点,每个节点保存字典中的一个键值对。

哈希表结构定义如下:

typedef struct dictht {
    dictEntry **table;  // 哈希表数组 每个元素都是 dictEntry 的指针,指向 dictEntry;
    unsigned long size; // 哈希表大小
    unsigned long sizemask; // 用来计算索引值 always: sizemask = size - 1
    unsigned long used; // 哈希表已有节点的数量
} dictht;

哈希表节点定义如下:

哈希表节点使用 dictEntry 结构表示,每个 dictEntry 结构都保存着一个键值对和冲突后的链表的下一个节点。

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 保存下一个 dictEntry 的地址,形成链表
} dictEntry;

其中,value 是一个联合体,可以保存多种数据类型。当value类型为 uint64_t 、int64_t 或 double时可以直接存储。其他类型需要在其他位置申请一段空间来存放,并用val指向这段空间来使用。

字典结构定义如下:

// location: dict.h
typedef struct dict {
    dictType *type;  // 指向 dictType 结构的指针
    void *privdata;  // 存储私有数据的指针,在 dictType 里面的函数会用到
    dictht ht[2];    // 两个哈希表,扩容时使用,后面会结合源码详细说明
    long rehashidx;  // 值为-1时,表示没有进行rehash,否则保存rehash执行到那个元素的数组下标 
    int16_t pauserehash; // >0 表示rehash暂停,<0 表示编码错误 
} dict;

dictType 结构定义如下

dictType 结构体定义了一系列操作key-value键值对的方法的函数指针,在实际运行时传入指定函数,就能实现预期的功能,有点运行时多态绑定的味道。

// 操作特性键值对的函数簇
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key); // 计算哈希值的函数
    void *(*keyDup)(void *privdata, const void *key);  // 复制key的函数
    void *(*valDup)(void *privdata, const void *obj);  // 复制value的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 对比key的函数
    void (*keyDestructor)(void *privdata, void *key);  // 销毁key的函数
    void (*valDestructor)(void *privdata, void *obj);  // 销毁value的函数
    int (*expandAllowed)(size_t moreMem, double usedRatio); // 扩容
} dictType;

字典整体结构可以用下图来描述:

3.1 hash冲突

当两个或两个以上的键被分配到哈希数组的同一个索引上面时,我们称这些键发生了冲突。Redis的哈希表使用拉链法解决hash冲突。

3.2 负载因子

负载因子 = used / size ; used 是哈希数组存储的元素个数,size 是哈希数组的长度。

负载因子越小,冲突越小;负载因子越大,冲突越大。

3.3 rehash

随着命令的不断执行,哈希表保存的减值对会逐渐增加或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表中的键值对过多或过少时,需要对哈希表的大小进行相应的扩展和收缩。而哈希表的扩展和收缩可以通过rehash来执行。rehash 就是将 ht[0] 中的节点,通过重新计算哈希值和索引值放到 ht[1] 哈希表指定的位置上。

扩容

  • 如果负载因子大于1,就会触发扩容,扩容的规则是每次翻倍;
  • 如果正在fork,执行持久化则不会扩容,但是,如果负载因子大于5,会立马扩容。

缩容

如果负载因子小于0.1,就会触发缩容。缩容的规则是:恰好包含used的2^n。

3.4 渐进式rehash

当哈希表中的元素过多时,如果一次性rehash到ht[1],庞大的计算量,可能导致redis服务在一段时间不可用。为了避免rehash对服务器带来的影响,redis分多次、慢慢的将ht[0]哈希表中的键值对rehash到ht[1]哈希表,这就是渐进式rehash。

核心思想:将整个rehash过程均摊到每次命令的执行中。

rehash的详细步骤

  1. 为 ht[1] 哈希表分配空间,此时字典同时拥有ht[0] 和 ht[1] 两个字典
  2. 将字典中的rehashidx设置为0,表示开始rehash
  3. 在rehash期间,每次对字典的增删改查,除了执行指定的命令外,还会顺带将ht[0] 中 rehashidx 索引上的所有键值对都rehash到ht[1]中,执行完rehash,rehashidx属性加一。注意:新增的键值对只能插入到ht[1]哈希表中,保证ht[0]的键值对只减不增。
  4. 随着操作的不断进行,最终ht[0]哈希表中的所有键值对都被rehash到ht[1]中。此时,将ht[0]释放掉,让ht[0] 指向ht[1],并设置rehashidx 为 -1,表示rehash完成。

四、字典常用 API

// 创建字典
dict *dictCreate(dictType *type, void *privDataPtr);
// 将键值对 key-val 插入到字典
int dictAdd(dict *d, void *key, void *val);
// 删除字典中指定 key 的键值对
int dictDelete(dict *d, const void *key);
// 获取指定key的value值
void *dictFetchValue(dict *d, const void *key);
// 将键值对 key-val 插入到字典,如果该key已经存在,则只更新val
int dictReplace(dict *d, void *key, void *val);

五、Rehash源码分析

5.1 添加元素步骤
// 1. 通过hash函数得到hash值
hash = dict->type->hashFunction(key);
// 2. 将hash值与对应哈希表的sizemask 进行 & 操作得到index
index= hash & d->ht[x].sizemask; // x = 0 or 1
// 3. 创建 dictEntry 节点,头插法插入到对应哈希表的index的位置
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
添加元素源码分析

redis使用 dictAdd() 方法往哈希表中添加元素。dictAdd 调用的是 dictAddRaw 方法,它会先通过_dictKeyIndex() 函数计算出table的index;再通过头插法将该节点插入到目标位置。

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;
  // 将 val 保存到 entry 节点
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictAddRaw() 函数

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 计算index
    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. */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    // 头插法 最快
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    // 将 key 保存在 entry 节点中
    dictSetKey(d, entry, key);
    return entry;
}

其中,在_dictKeyIndex() 函数计算index的时候,会调用 _dictExpandIfNeeded() 函数判断是否满足扩容的条件。其中有个条件是依赖于 dictTypeExpandAllowed(d) 的返回值。

_dictKeyIndex() 函数

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 */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

_dictExpandIfNeeded() 函数

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    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. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

dictTypeExpandAllowed() 函数

static int dictTypeExpandAllowed(dict *d) {
    if (d->type->expandAllowed == NULL) return 1;
    return d->type->expandAllowed(
                    _dictNextPower(d->ht[0].used + 1) * sizeof(dictEntry*),
                    (double)d->ht[0].used / d->ht[0].size);
}

可以看到,如果 dictType 中没有设置 expandAllowed 函数,则直接返回真;如果设置了expandAllowed 函数,就需要执行完相应的函数才能确定是否可以扩缩容。这就是 dictType 的一个典型的应用场景。

5.2 rehash 源码分析

该函数每次从rehashidx开始的位置,固定扫描 10 个bucket,如果对应的bucket中有数据就 rehash 到 ht[1]中。

static void _dictRehashStep(dict *d) {
    if (d->pauserehash == 0) dictRehash(d,1);
}
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++;
            // 如果 经过 n*10 次,还是为空,本次rehash结束
            if (--empty_visits == 0) return 1;
        }
        // 找到要rehash的bucket
        de = d->ht[0].table[d->rehashidx];
        // 遍历该bucket对应的dictEntry链表
        while(de) {
            uint64_t h;
            nextde = de->next;
            // 为该dictEntry 获取在ht[1]中的index 
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 头插法 de->next 指向 ht[1].table[h]的第一个元素 (可能为NULL,可能有元素)
            de->next = d->ht[1].table[h];
            // 更新ht[1].table[h]的值指向该 dictEntry
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde; // 更新de,继续遍历,直至为空
        }
        // 将ht[0]中 d->rehashidx 对应的bucket清空
        d->ht[0].table[d->rehashidx] = NULL;
        // 更新rehashidx
        d->rehashidx++;
    }
    // 检查整个ht[0]表,rehash是否完成
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table); // 释放ht[0]
        d->ht[0] = d->ht[1];   // 让ht[0] 指向 rehash后的 ht[1]
        _dictReset(&d->ht[1]); // 重置 ht[0], 以备下次rehash
        d->rehashidx = -1;
        return 0;
    }
    /* More to rehash... */
    return 1;
}

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
23天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
27天前
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
55 8
|
27天前
|
存储 NoSQL Java
介绍下Redis 的基础数据结构
本文介绍了Redis的基础数据结构,包括动态字符串(SDS)、链表和字典。SDS是Redis自实现的动态字符串,避免了C语言字符串的不足;链表实现了双向链表,提供了高效的操作;字典则类似于Java的HashMap,采用数组加链表的方式存储数据,并支持渐进式rehash,确保高并发下的性能。
介绍下Redis 的基础数据结构
|
22天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树
|
22天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
18天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
94 9
|
9天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
19 1
|
12天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
15天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
17天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
46 4

热门文章

最新文章

下一篇
无影云桌面