数据结构-字典
字典结构
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
ht是一个包含两个哈希表的数组,一般情况下字典只使用ht[0],只有在对ht[0]的哈希表进行rehash时才会使用ht[1]。
哈希表数据结构
dictht是哈希表的数据结构,dictEntry是每个entry元素的数据结构。
typedef struct dictht { //指针数组,这个hash的桶 dictEntry *table; //元素个数 unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
- table属性是一个数组,数组中的每个元素都是一个指向哈希表节点的指针,每个哈希表节点都保存着一个键值对。
- size属性记录了哈希表的大小,也即table数组的大小。
- used属性记录了哈希表目前已有节点的数量。
- sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上。
哈希表节点数据结构
typedef struct dictEntry { // 键 void *key; // 值 union { // 指向具体redisObject void *val; // uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next; } dictEntry;
- key属性保存键值对中的键
- v属性保存键值对中的值,其中键值对的值可以是一个指针,指向具体的redisObject,或者是一个uint64_t整数,或者是一个int64_t整数
- next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突(collision)的问题
哈希冲突
Redis解决哈希冲突的方式,就是链式哈希(头插法)。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
rehash(渐进式)
负载因子(used/size)来表述哈希冲突的激烈程度,负载因子越大,冲突越激烈。
size表示哈希表的大小,也就是哈希桶的个数。
used表示有多少个 键值对实体(dictEntry)。
扩容的时机:
- 负载因子≥1,同时,哈希表被允许进行 rehash(实例正在生成 RDB 或者重写 AOF不允许rehash)。
- 负载因子≥5。
上面说到字典存在2个哈希表,一开始,当你刚插入数据时,默认使用ht[0],此时的ht[1]并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
- 给 ht[1] 分配更大的空间,例如是当前 ht[0] 大小的两倍。
- 把 ht[0] 中的数据重新映射并拷贝到 ht[1] 中。
- 释放哈希表 ht[0] 的空间。
到此,我们就可以从哈希表 ht[0] 切换到哈希表 ht[1],用增大的哈希表ht[1]保存更多数据,而原来的哈希表 ht[0] 留作下一次 rehash 扩容备用。
第二步涉及大量的数据拷贝,如果一次性把 ht[0] 中的数据都迁移完,会造成 Redis 线程阻塞,采用了渐进式 rehash。
Redis 每处理一个请求时,从哈希表 ht[0] 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 ht[1] 中;等处理下一个请求时,再顺带拷贝 ht[0] 中的下一个索引位置的 entries。如下图所示:
把rehashidx置为0表示rehash开始,rehashindex记录原hash表的拷贝进度,每次拷贝进行递增,如果拷贝完成,设置为-1。
在rehash过程中,读、新增、更新、删除操作rehashidx++。读操作会先读原hash表,读不到再读新hash表;新增操作只在新hash表执行;更新、删改操作两个表都要执行。
渐进式rehash这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,Redis本身还会有一个定时任务在执行rehash,如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个rehash的过程。