Redis 中的 dict
数据结构(字典)是一个重要的数据结构,用于存储键值对。其底层实现是通过哈希表来实现的,结合了哈希表和链表的方式来处理键值对的存储和检索。
组成源码:
Dict其实是由dict、dictht、dictEntry三部分组成的,它们的源码分别是:
dict(字典):
typedef struct dictType { unsigned int (*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; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; int iterators; } dict;
我们可以发现1个dict有2个dictht,这个在后面讲增量rehash有大用!
dictht(全局哈希表):
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
dictEntry(哈希节点):
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
它们之间的关系用图可以表示为:
Dict特点:
使用Dic可以实现O(1)的平均复杂度的查找、插入和删除操作,在处理大量键时表现地尤为出色。redis数据结构中的set、zset中不允许出现重复数据的特性可以通过Dic去实现。
Dict与哈希冲突:
哈希表必定会存在哈希冲突的问题,在redis底层是使用链地址法解决哈希冲突问题的,并且采用的是头插法。
Dict的Rehash:
Dict的Rehash分为两种,一种是扩容,一种是缩容。
在讲rehash条件之前,我先讲一下哈希表的加载因子。
加载因子:
哈希表的加载因子表示哈希表中已存储元素与哈希表容量的比值。加载因子的大小直接影响哈希表的性能和空间利用率。
-
加载因子 = 哈希表中已存储元素数量 / 哈希表容量
加载因子的作用:
-
性能影响:加载因子控制着哈希表的平均填充程度,合适的加载因子能够保持哈希表的操作效率。
-
空间利用率:通过加载因子调整哈希表的填充程度,可以在保持效率的同时避免空间浪费。
Rehash条件:
扩容:
-
哈希表的加载因子>=1,并且没有执行bgsave等后台进程
-
哈希表的加载因子>5
-
扩容后的新size大小为第一个大于等于dict.ht[0].used+1的2^n
缩容:
-
每次删除元素时进行检查,当负载因子<0.1,会对哈希表进行收缩
-
缩容后的新size大小为第一个大于等于dict.ht[0].used的2^n(不能小于4)
增量式Rehash(渐进式Rehash)过程与优势:
过程:
(1) 默认有两个全局哈希表,另一个哈希表容量是当前的2倍。
(2) 防止一次性拷贝带来的阻塞,而采用渐进式拷贝。处理一次请求,顺带拷贝一个DictEntry。
(3) 释放原先哈希表空间,等待下次扩容时用。
优势:
-
减少阻塞:
-
由于数据迁移是逐渐进行的,渐进式 Rehash 能够减少对整个哈希表的阻塞时间,降低系统在数据迁移过程中的影响。
-
-
复杂度分摊:
-
将数据迁移过程分解为多个小步骤,避免了一次性大规模数据搬迁的复杂性和阻塞,使得操作更加平滑。
-
-
操作原子性:
-
在渐进式 Rehash 过程中,Redis 保证了对哈希表的操作是原子性的,避免由于并发操作导致数据一致性问题。
-
总结:
渐进式 Rehash 是 Redis 中对 dict 数据结构进行扩容或缩小时的一种优化策略,通过逐步迁移数据,减少系统阻塞时间,保证操作的稳定性和性能。