reids是什么就不过多描述了,咱直接进入主题
我们都知道redis的5大数据类型:string,list,hash,set与zset。但其实就redis而言,它的整体是一个dict类型的,是redis中所有key到value的映射。
一、dict
下面写到:每个字典都有两个这样的对旧表到新表执行增量重新散列。
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dict {
dictType *type; //dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据
void *privdata; //私有数据,保存着dictType结构中函数
dictht ht[2]; //两张哈希表
long rehashidx; //rehash的标记,rehashidx == -1,表示没有进行 rehash
int16_t pauserehash; //正在迭代的迭代器数量
} dict;
typedef struct dictht {
dictEntry **table; //存放一个数组的地址,数组中存放哈希节点dictEntry的地址
unsigned long size; //哈希表table的大小,出始大小为4
unsigned long sizemask; //用于将hash值映射到table位置的索引,大小为(size-1)
unsigned long used; //记录哈希表已有节点(键值对)的数量
} dictht;
既然是hash,那么就一定会有扩容的操作。看看具体是怎么做的
这里着重说一下两张哈希表,先说结论,dict数据与java中的map相似,主要是根据负载因子(源码中的参数load_factor)进行扩容的操作。
- 负载因子
- load_factor = ht[0].used / ht[0].size,这是计算负载因子的公式
- 在正常情况下,当 load_factor 大于1时,会进行扩容操作
- 在RDB/AOF(做持久化,可以理解为mysql的binlog)阶段,load_factor 默认大于5才会进行扩容操作。
- redis会fork一个子进程,在子进程期间,来提高负载因子,可以避免在子进程中进行rehash,规避不必要的内存写入,节约内存,另外也是为了尽可能减少内存页过多分离,系统需要更多的开销去回收内存。
- 扩容
- 扩容操作就是rehash的操作,redis在rehash是占用主线程的,所以采用渐进式rehash(即每次只移动一个链表)。这样避免了客户端的指令堆积与阻塞
- 是在重新rehash的时候,ht[0]和ht[1]都有效。在平常,就只有ht[0]生效,而ht[1]是空的
- 当rehash完成,则会将0进行释放,将1设置为0,在将1初始化成新的hash表
- 在开始和完成rehash,会去更新rehashidx = 0(表示rehash开始工作,每进行一次rehash,rehashidx += 1)和 rehashidx = -1(结束rehash)
- 缩容
- 负载因子小于0.1
- reash时的CURD
- 插入。只会在ht[1]上进行新增
- 查询,更新与删除:会在ht[0]和ht[1]上同时执行,如果ht[0]上没找到,则会在ht[1]上操作。
注:触发rehash的操作有查询、插入和删除元素。插入与删除很好理解,但是查询又是为什么呢?
其原因就在于当哈希桶中的链表过长时,那么查询性能会显著降低(链表的查找时间复杂度为O(N)),Redis为了避免类似的问题从而会进行Rehash操作
粗略的画了下rehash的流程,详细的可以看redis的rehash,这里讲的是老版本的redis,但是rehash基本没变,所以我给他放上来