文章目录

  • 1. Dict
  • 1.1 Dict的实现
  • 1.2 Dict扩容
  • 1.3 Dict收缩
  • 1.4 Dict的rehash
  • 2.5 总结


1. Dict

Redis 是一个键值型的数据库,可以根据键实现快速的增删改查,而键与值得映射关系正式通过Dict来实现的。

1.1 Dict的实现

Dict由三部分组成:哈希表(DictHashTable)、哈希节点(DIctEntry)、字典(Dict)

Redis 的 Dict 使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。接下来我们来介绍一下它们的实现

DictHashTable

typedef struct dictht {
	// entry数组,数组中保存的是指向entry的数组的指针
	dictEntry **table;
	// 哈希表大小
	unsigned long size;
	// 哈希表大小的掩码,总是等于 size-1
	unsigned long sizemask;
	// 该哈希表已有的节点的数量
	unsigned long used;
} dictht;

DictEntry
如果出现hash冲突,这里采用头插法,将冲突的数据插入表头 next指向被插的节点,因为Redis是单线程,所以不会出现循环链表

typedef struct dictEntry {
	// 键
	void *key;
	union {
		void *val;
		uint64_t u64;
		int64_t s64;
		double d;
	} v; // 值:可以位union中的热
	// 下一个 Entry 的指针
	struct dictEntry *next;
}dictEntry;

Dict

typedef struct dict {
	// dict类型,内置不同的hash函数
	dictType *type;
	// 私有数据,在做特殊hash运算时用
	void *privdata;
	// 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般为控,rehash时使用
	dictht ht[2];
	// rehash的进度,-1表示未进行
	long rehashidx;
	// rehash是否暂停,1暂停,0继续
	int16_t pauserehash;
} dict;

Dict在Redis中要用到的场景非常多,不同场景下的hash函数可能是不一样的,所以Redis就封装了好多的hash函数,然后给他封装到了 dictType中,结构体如下所示:

hash java实现 redis redis hash 底层实现_Redis

下图为dict示例

hash java实现 redis redis hash 底层实现_Redis_02

1.2 Dict扩容

Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。

Dict在每次新增键值对时都会检查负载因子(LoadFactor = used/size),满足以下两种情况时会触发哈希表扩容:

  • 哈希表的LoadFactor >=1,并且服务器没有执行BGSAVE或者BGREWRITEAOF 等后台进程;
  • 哈希表的LoadFactor > 5 ;

扩容后的大小为第一个大与等于 used + 1的2n,源码如下图所示:

  • 如果正在rehash,就直接返回ok
  • 如果哈希表为空,则初始化哈希表为默认大小:4
  • 如果used >= size,就相当于 used/size 负载因子达到1以上,并且dict_can_resize等于true时(dict_can_resize参数:当BGSAVE和BGREWRITEAOF这样的命令在执行时,该变量会置为0)
    或者负载因子(used/size) > dict_force_resize_ratio(dict_force_resize_ratio的值为5),就进行 dictExpand,就是扩容

hash java实现 redis redis hash 底层实现_sed_03

1.3 Dict收缩

Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当LoadFactor < 0.1时,会做哈希表收缩。

收缩到什么大小呢?如下图源码,它会做判断,收缩至当前的元素个数,但不能比初始化大小4还要小,

下图为源码:

hash java实现 redis redis hash 底层实现_Redis_04

1.4 Dict的rehash

Dict的rehash并不是一次性完成的。他是分多次、渐进式的完成,流程如下:

  1. 计算新hash表的size,值取决于当前要做的是扩容还是收缩:
    - 如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的 2^n
    - 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n(不得小于4)
  2. 按照新的size申请内存空间,创建dictht,并赋值给dict.ht[1]
  3. 设置dict.rehashidx = 0,标示开始rehash
  4. 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]
  5. 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
  6. 将rehashidx赋值为-1,代表rehash结束

注意: 在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht0]的数据只减不增,随着rehash最终为空

2.5 总结

Dict结构:

  • 类似java的HashTable,底层是数组加链表来解决哈希冲突
  • Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash

Dict的扩容伸缩:

  • 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
  • 当LoadFactor小于0.1时,Dict收缩
  • 扩容大小为第一个大于等于used + 1的2n
  • 收缩大小为第一个大于等于used的2n
  • Dict采用渐进式rehash,每次访问Dict时执行一次rehash
  • rehash时ht[0]只减不增,新 增操作只在ht[1]执行,其它操作在两个哈希表

内容参考《Redis设计与实现》、Redis 6.2.0源码