Redis源码解析:dict与迭代器

本文详细介绍了Redis中字典(dict)的数据结构,包括dictType、dictht、dictEntry等组件,以及其与Java HashMap的相似之处。重点讨论了Redis如何使用渐进式rehash解决扩容问题,避免一次性rehash带来的性能影响。在rehash过程中,新老哈希表并存,通过索引计数器逐步迁移元素,确保命令执行效率。此外,还提到了在rehash期间处理数据变更的操作策略。
摘要由CSDN通过智能技术生成

在这里插入图片描述

介绍

在之前的文章中我们提到,Redis中的数据是放在一个字典中的。

例如当我们执行如下命令后,redis的字典结构如下

set bookName redis;
rpush fruits banana apple;

在这里插入图片描述
在这里插入图片描述
Redis中hash也用dict来实现的,zset中存储value和score值的映射关系也是用dict来实现的。

其实Reids中dict的实现和Java中HashMap的实现很类似,也是采用数组+链表的形式实现的。结构如下

typedef struct dict {
	// 类型特定函数
    dictType *type;
    void *privdata;
    // hash表
    dictht ht[2];
    // rehash索引,当rehash不在进行时,为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

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

privdata属性保存了需要传给那些类型特定函数的可选参数

typedef struct dictType {
	// 计算哈希值的函数
    uint64_t (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

ht是一个包含2个项的数组,数组中的每个项是一个dictht表,一般情况下字典只使用ht[0]哈希表,ht[1]哈希表只有在对h[0]哈希表进行rehash时使用

rehashidx也是一个和rehash有关的属性,记录了rehash的进度。当值为-1时,表示当前没有进行rehash

iterators,迭代器,遍历使用。

hash表的结构如下

typedef struct dictht {
	// hash表数组
    dictEntry **table;
    // hash表大小,即数组长度
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,值为size-1
    unsigned long sizemask;
    // hash表已有节点的数量
    unsigned long used;
} dictht;

table是数组,用来存储键值对,元素的类型是dictEntry
size为数组的长度
sizemask用来计算键的索引值,sizemask=size-1。数组的初始容量是4,当元素增加需要扩容时,新扩容的大小为当前的一倍,即数组的容量大小只能为4,8,16,32… sizemask的值为3,7,15,31。计算索引值的步骤如下idx=hash & sizemask就行。和HashMap的数组长度恒为2的n次方的原因相同,就是为了提高计算索引值的速度
used数组中已存键值对个数

hash表中每个元素的结构如下

typedef struct dictEntry {
	// 键
    void *key;
    // 值,可以是一个指针,或者一个uint64_t整数,或者一个int64_t整数等
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 后继指针
    struct dictEntry *next;
} dictEntry;

key存储的是键值对中的键
值是一个联合体,存储的是键值对中的值,在不同场景下使用不同字段。例如用字典存储整个redis数据库中的键值对时,用的是*val字段,可以指向不同类型的值。当字典被用作记录过期键的过期时间时,用的是s64字段存储
当出现hash冲突时,通过next指针指向下一个元素,插入元素用的是头插法

一个完整的Redis字典的数据结构如下
在这里插入图片描述
可以看到和HashMap不同的地方是有两个数组,这主要和dict的扩容方式有关,我们后面会提到,即渐进式rehash

增加元素

增加元素的过程和HashMap类似。

  1. 先根据key定位数组的索引值,将元素放到索引对应的桶上
  2. 当两个以上的键被放到数组的同一个索引上面时,用链表法来解决hash冲突

扩容

什么时候会进行扩容?

我们先来回忆一下Java中的HashMap是何时扩容的?HashMap中有这样一个公式
threshold = capacity * load factor,即扩容的阈值=数组长度 * 负载因子,如果hashmap数组的长度为16(默认值),负载因子为0.75(默认值),则扩容阈值为16*0.75=12。当HashMap中放的键值对超过12时,就会进行扩容。

负载因子=总的键值对/数组长度,即当负载因子>0.75会进行扩容

在Redis中会根据如下两种情况来进行扩容

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

负载因子的计算公式如下

// 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

当负载因子小于0.1时,程序对字典执行收缩操作。Java中的HashMap是没有收缩操作的哈

渐进式rehash

当需要扩容的时候,HashMap会一次性将旧数组下的元素rehash到新数组下。
因为Redis是用一个线程来处理命令,当字典的元素很多,一次性rehash耗时会增加,影响后续命令的执行。为来解决这个问题,Redis采用了渐进式rehash,即并不是一次性将ht[0]的键值对rehas到ht[1],而是分多次,渐进式的将h[0]的键值对rehash到ht[1]

rehash的过程如下

  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] ,当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

图示如下,注意属性的变化
在这里插入图片描述
rehash期间有数据变化应该怎么办?

操作类型过程
增加将键值对增加到ht[1]里面,不对ht[0]进行操作
删除先删除ht[0] ,没找到再删除ht[1]
修改先修改ht[0] ,没找到再修改ht[1]
查询先查询ht[0] ,没找到再查询ht[1]

迭代器

安全迭代器:
非安全迭代器:

参考博客

[1]https://juejin.cn/post/6872881786314194951
[2]https://studyidea.cn/redis-dict
[3]https://my.oschina.net/u/4579410/blog/4890414
迭代器
[4]https://www.jianshu.com/p/6b9591979d51
[5]https://juejin.cn/post/6844903657104736269
[6]https://segmentfault.com/a/1190000020458561

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java识堂

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

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

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

打赏作者

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

抵扣说明:

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

余额充值