浅析Redis④:字典dict实现

什么是dict?

在 Redis 中,dict 是指哈希表(hash table)的一种实现,用于存储键值对数据。dict 是 Redis 中非常常用的数据结构之一,用于实现 Redis 的键空间。

在 Redis 源码中,dict 是一个通用的、高性能的哈希表实现,它采用开放寻址法(open addressing)作为冲突解决方案,并且具有良好的性能特征。

在 Redis 的源码中,dict 被用于实现 Redis 中的数据库、哈希键(hash key)等数据结构。通过 dict 这一数据结构,Redis 能够高效地实现键值对的存储和检索,保证了 Redis 的高性能和快速响应。

dict 在 Redis 中扮演着非常重要的角色,是支撑 Redis 数据存储和操作的基础之一。

核心特性

  • 开放寻址法:Redis 中的字典采用了开放寻址法作为冲突解决方案。在发生哈希冲突时,它会通过线性探测(linear probing)的方式来寻找下一个可用的位置。
  • 渐进式 rehashing:Redis 的字典实现中采用了渐进rehashing 策略,这意味着在进行扩容或缩小操作时,不会一次性地重新分配所有元素,而是逐步迁移键值对,以降低对服务的影响。
  • 哈希表的大小:Redis 中的字典会根据当前包含的元素数量动态调整哈希表的大小,以保证较低的负载因子,从而保持较好的性能。
  • 链表:在哈希表的每个槽位上,可以形成一个链表,用于处理哈希冲突时的多个元素。当链表长度过长时,Redis 会将链表转换为更高效的哈希表。

总的来说,Redis 中的字典通过哈希表实现,结合了开放寻址法、渐进式 rehashing 等策略,以及动态调整大小等特性,为 Redis 提供了高效的键值对存储和检索能力。这些特性使得 Redis 的字典在处理大量数据时依然能够保持良好的性能。

聚焦问题

1、dict的数据结构长什么样子,对比Java的HashMap

2、dict如何插入数据,并且如何解决hash冲突的

3、dict的rehash策略

dict的数据结构

c 复制代码
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
c 复制代码
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dict的本质是使用一个数组来存储key-value键值对,数组中的每个元素都是一个指向dictEntry结构的指针,而每一个dictEntry则保存了key-value键值对,是否听起来似曾相识,与Java中HashMap的结构非常相似

之所以说非常相似,而不是一模一样,是因为关于hash寻址冲突后,对同一个数组位置上元素的存储结构,并不完全一致,HashMap对于同一个数组位置的元素初始会使用链表的形式组装,而链表的长度达到一定阈值后,则转为红黑树的形式组装。

而dict并没有这种处理方式,仅会使用链表组织元素,这样的好处显而易见,链表的实现难度一定是比红黑树简单,也更易于调试,但是对与hash寻址算法的要求也更高,需要尽可能的保证少量的hash冲突,即寻址计算尽可能分散,否则链表长度过长,会影响取值的效率。

渐进式rehash

Redis 使用渐进式 rehash 来实现哈希表的扩容和缩容。渐进式 rehash 是一种在 Redis 服务不中断的情况下进行哈希表 rehash 的方法,它将 rehash 操作分解成多个小步骤,每次执行一小部分 rehash 操作,以避免阻塞服务。

渐进式 rehash 的主要步骤如下:

  1. 创建新哈希表: 当需要进行哈希表扩容或缩容时,Redis 首先会创建一个新的哈希表,其大小为原哈希表的两倍或更小。
  2. 迁移数据: 然后,Redis 会将原哈希表中的数据逐步迁移到新哈希表中。每次迁移一小部分数据,而不是一次性全部迁移。这样做的好处是,在迁移的过程中,Redis 仍然可以接受读取请求,并且只有在写入数据时才会阻塞。
  3. 逐步完成: 当所有的数据都迁移完成后,Redis 将原哈希表替换为新哈希表。这个过程是逐步进行的,不会中断服务。

渐进式 rehash 的优点包括:

  • 减少阻塞时间: 由于将 rehash 操作分解成多个步骤执行,因此可以大大减少阻塞时间,提高系统的稳定性和可用性。
  • 逐步进行: rehash 操作是逐步进行的,可以平滑地将数据从旧哈希表迁移到新哈希表,不会对系统造成过大的负担。
  • 保证读写服务: 在 rehash 过程中,Redis 仍然可以提供读取服务,只有在写入数据时才会阻塞。

需要注意的是,虽然渐进式 rehash 可以减少阻塞时间,但在 rehash 过程中,仍然会占用一定的系统资源,可能会对性能产生一定的影响。因此,在进行 rehash 操作时,需要谨慎安排时间,并在低负载时执行。

下面我们来一起看一下,具体实现逻辑:

c 复制代码
/* This function handles 'background' operations we are required to do
 * incrementally in Redis databases, such as active key expiring, resizing,
 * rehashing. */
void databasesCron(void) {
    
    if (server.active_expire_enabled && server.masterhost == NULL)
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);

    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
        static unsigned int resize_db = 0;
        static unsigned int rehash_db = 0;
        int dbs_per_call = CRON_DBS_PER_CALL;
        int j;

        /* Don't test more DBs than we have. */
        if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;

        /* Resize */
        for (j = 0; j < dbs_per_call; j++) {
            tryResizeHashTables(resize_db % server.dbnum);
            resize_db++;
        }

        /* Rehash */
        if (server.activerehashing) {
            for (j = 0; j < dbs_per_call; j++) {
                // rehash执行
                int work_done = incrementallyRehash(rehash_db % server.dbnum);
                rehash_db++;
                if (work_done) {
                    /* If the function did some work, stop here, we'll do
                     * more at the next cron loop. */
                    break;
                }
            }
        }
    }
}
c 复制代码
int incrementallyRehash(int dbid) {
    /* Keys dictionary */
    if (dictIsRehashing(server.db[dbid].dict)) {
        dictRehashMilliseconds(server.db[dbid].dict,1);
        return 1; /* already used our millisecond for this loop... */
    }
    /* Expires */
    if (dictIsRehashing(server.db[dbid].expires)) {
        dictRehashMilliseconds(server.db[dbid].expires,1);
        return 1; /* already used our millisecond for this loop... */
    }
    return 0;
}
c 复制代码
/* 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;

    // 每次执行100次,这就是渐进
    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}
c 复制代码
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    // 注意此处的边界条件,每次传入100,也就是只会执行100次循环
    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 */
        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;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    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结束

Redis的dict与Java中的HashMap,实现上有什么区别

Redis 的 dict 是在 C 语言中实现的字典结构,而 Java 中的 HashMap 是在 Java 编程语言中实现的哈希表结构。它们之间有以下几个主要区别:

  • 语言差异:dict 是 Redis 自行实现的数据结构,使用 C 语言编写。而 HashMap 是 Java 标准库中提供的类,使用 Java 编写。
  • 内存管理:Redis 的 dict 使用了手动内存管理,需要手动分配和释放内存空间。而 Java 的 HashMap 使用垃圾回收机制自动管理内存,无需手动处理。
  • 线程安全性:Redis 的 dict 不是线程安全的,如果多个线程同时对同一个 dict 进行读写操作,可能会导致数据不一致或者程序崩溃。而 Java 的 HashMap 是非线程安全的,但可以通过使用 ConcurrentHashMap 等线程安全的 Map 实现来解决并发访问的问题。
  • 扩容机制:Redis 的 dict 在进行扩容时使用了渐进式 rehash 算法,将扩容操作分摊到多个操作中,避免了大规模数据迁移带来的性能问题。而 Java 的 HashMap 在扩容时需要重新计算哈希值,并将元素重新分布到新的桶位中。
  • 功能差异:Redis 的 dict 是为了满足 Redis 数据库的需要而设计的,支持一些特定的功能,如过期时间等。Java 的 HashMap 是通用的哈希表实现,提供了丰富的方法和功能,适用于多种应用场景。

总体而言,Redis 的 dict 和 Java 的 HashMap 在实现上有一些差异,主要是因为它们所运行的环境和使用的编程语言不同。它们在性能、线程安全性、扩容策略等方面都有各自的特点和优化。

Redis dict的rehash和Java HashMap的rehash,有什么不同

Redis 的 dict 和 Java 的 HashMap 在 rehash(重新哈希)操作上有一些区别:

  • 触发条件:Redis 的 dict 在进行 rehash 操作时,是在字典的负载因子(load factor)超过阈值时触发的,即元素数量超过容量的一定比例。而 Java 的 HashMap 在进行 rehash 操作时,是在桶位(buckets)的使用情况超过阈值时触发的,即某个桶位中元素的数量超过链表长度的阈值(8)。
  • 扩容方式:Redis 的 dict 采用了渐进式 rehash 算法,将扩容操作分摊到多个操作中,避免了大规模数据迁移带来的性能问题。具体而言,dict 在进行 rehash 时会逐步将 ht[0] 中的元素迁移到 ht[1] 中,直到完成整个 rehash 过程。而 Java 的 HashMap 使用的是一次性扩容的方式,在 rehash 时会创建一个更大的桶位数组,并将所有元素重新计算哈希值后放入新的桶位中。
  • 并发处理:Redis 的 dict 在 rehash 过程中,仅支持单线程执行,不支持并发访问。这意味着在 rehash 过程中,不能同时进行读取和写入操作,否则可能导致数据不一致。而 Java 的 HashMap 在 rehash 过程中,不会影响并发访问的正常进行。通过使用读写锁或者分段锁等机制,可以在不阻塞其他线程的情况下进行并发读取和写入操作。

Redis 的 dict 和 Java 的 HashMap 在 rehash 操作上有一些差异。Redis 的 dict 使用了渐进式 rehash 算法,避免了大规模数据迁移带来的性能问题,但不支持并发访问。而 Java 的 HashMap 在 rehash 过程中可以进行并发访问,但需要一次性扩容,并重新计算哈希值。这些差异是由于它们所处的环境和应用场景的不同导致的。

相关推荐
勘察加熊人32 分钟前
fastapi房产销售系统
数据库·lua·fastapi
m0_748254661 小时前
MySQL和SQL server的区别
数据库·mysql
补三补四1 小时前
Yashan DB 实例
数据库·oracle·dba
食指Shaye1 小时前
Chrome 中清理缓存的方法
前端·chrome·缓存
morris1311 小时前
【redis】布隆过滤器的Java实现
java·redis·布隆过滤器
椰椰椰耶1 小时前
【redis】全局命令set、get、keys
数据库·redis·缓存
月落星还在1 小时前
Redis 内存淘汰策略深度解析
数据库·redis·缓存
五行星辰2 小时前
Java链接redis
java·开发语言·redis
左灯右行的爱情2 小时前
Redis- 切片集群
数据库·redis·缓存
LKAI.2 小时前
MongoDB用户管理和复制组
linux·数据库·mongodb