Redis数据结构---字典,哈希表,dict 或java中的map,数据使用key -> value的形式存储,整个redis数据库就是基于字典实现,api见hash

REDIS的hash实现原理和java的HashMap十分相似,可参考阅读

理解redis的hash实现,就要先理解一下三个结构  dictEntry, ditht, dict

哈希表节点 dictEntry {

void  *key //键值

union{void *val; uint64_tu64; int64_ts64} v  //值    可以是指针,可以是uint_64_t整数 或者int64_t整数

struct dictEntry *next;  //指向下一个哈希节点,形成链表的结构(同java hashmap中的entry)

}

哈希表 dictht{

 dictEntry **table;  //一个元素为dictEntry 的数组

 long size; // 哈希表的大小

 long sizemask; // 哈希表大小掩码,用于计算存储时所在的数组下标  大小总是等于size-1

 long used;  //当前哈希表已有的节点数量

}

哈希表有个负载因子 的概念,load_factor = used/size  即已使用 除以总数size的结果

而提供api给我们开发者,即直接使用的dict实现如下

字典 dict{

  dictType *type //指针,特定类型的函数  redis中有定义,指向了一族函数,用以实现针对键值对的操作,这同list的dup free match一样,也是多太的一种,是dict可以存不同类型的键值对

  void *privdata //私有数据  用以使用dictType中定义的特定函数时传入的可选参数

  dictht ht[2]  //元素为哈希表的数组,长度为2,即保存着2个哈希表

  in trehashidx // rehash索引,当不在进行rehash时为-1

}

当向一个dict中set一个键值A对时,会先使用hash算法根据键值计算出一个数字,即哈希表中数组的下标,该键值对就存放在此位置上,

如果再有一个键值B对被set存放进入此dict时,hash算法根据键值计算出的数字同上,即为键冲突,也叫哈希冲突,这时hash表会使用头插法,将B的dictEntry的next设置为A,形成链表的数据结构

 

随着dict的操作,键值对会有增多和减少,为了是负载因子在合理范围内,会产生rehash,其步骤简单如下:

当没有rehash的普通情况下,dict中的ht[2] 数组总是使用ht[0] 对应的哈希表来保存数据,当需要扩容或缩减时,会为ht[1]分配对应的存储空间,其大小算法是

扩容时,大小是第一个大于ht[0]的size的 2的n次方,缩小时,是第一个小于ht[0]长度的2的n次方(可参考java的Hashmap中的tablesizefor算法)

当ht[1]分配好空间后,会把ht[0]上的所有数据复制到ht[1]上,之后ht[0]变为空表,是否,把ht[1]移到ht[0]上继续使用

如果redis在执行BGSAVE或BAREWRITEAOF命令操作,会在当前redis服务现场中创建子进程,使用的是写时复制技术优化子进程的使用效率!

当没有子进程存在时,负载因子大于等于1时就会进行rehash,当存在子进程时,负载因子需要大于等于5才会进行rehash

当负载因子小于0.1时,也会进行收缩操作的rehash!

这样设计的目的就是尽量的使子进程工作期间,不要有rehash操作的产生,避免不必要的内存浪费

 

rehash使用的是渐进式rehash

它不会一次性的吧所有的ht[0] 中的数据一下子复制到ht[1]中,而是在增删改查操作发生时,每发生一次就复制一个值过去,同时对rehashidx做+1,避免了集中的大量的运算而导致redis夯死,值得注意的是,每次新增操作会把新增的值放入ht[1]中,同时复制一个ht[0]中数据到ht[1]中!同时,因为完整数据存在于2个hash表中,所有查询时是先查ht[0],再查ht[1]!循序渐进的复制,最终复制完成