dict为redis服务器中出现的使用最为频繁的复合型数据结构,不但在hash类型和zset中使用了dict结构,整个redis数据库就是一个大的字典表,带有过期时间的key也组成一个大的字典表.
1.dict的内部结构
1.1 dict的内部结构
typedef struct dict { dictType *type;//dictType中定义了很多dict中常用的方法,具体见dictType void *privdata;//一些私有数据 dictht ht[2];//两个dictht数组,正常只有一个在用,只有当rehash时才会使用两个 long rehashidx; /* dic是否正在进行rehash,当不为-1时,说明正在rehash */ int iterators; /* 遍历器的个数*/ } dict;
ht[2]:里面存放了两个dictht类型的数据,dictht用于保存真正的数据,但是通常情况下只会使用一个dictht保存数据,只有当dict进行rehash的时候才会两个同时使用,关于rehash的详情见rehash小节.
rehashidx:dict是否正在rehash的标识,如果值为-1,标识dict没有正在进行rehash,否则标识dict正在进行rehash,并且rehashidx的值表示dictht中还没有进行rehash的键值对的数量.
dictType和privdata属性是针对不同类型的键值对为创建多态字典而设置的.dictType中定义了一组操作特定类型键值对的函数,privdata属性则保存了需要传给那些类型特定函数的可选参数
1.2 dictType的内部结构
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;
1.3 dictht的内部结构
typedef struct dictht { dictEntry **table; //dictEntry数组保存真正的键值对 unsigned long size;//table数组的长度 unsigned long sizemask;//table数组的长度-1,为了方便计算键值对保存在table中的位置而定义的变量 unsigned long used;//dictht中已经存在的键值对数量 } dictht;
size:是table数组的长度,这个长度是固定的,并不会因为插入和删除元素而改变
used:保存在table中元素的数量,即当前dictht中有多少元素
1.4 dictEntry的内部结构
typedef struct dictEntry { void *key; //键值对中的key union { void *val; uint64_t u64; int64_t s64; double d; } v; //键值对中的value,对不同的value选取不同的值 struct dictEntry *next;//指向下一个dictEntry的指针 } dictEntry;
key,v:key,v分别保存了键值对的键和值.
next:记录数组中统一个位置的下一个dictEntry的.
2.hash冲突
因为键值对在table中的位置是根据key的hashCode决定的,所以可能会发生hash冲突的情况,Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(头插法,复杂度为O(1)),排在其他已有节点的前面。
3.扩容
一般来讲,当dictht中used和size相等的时候就会触发redis的扩容操作,默认redis扩容的时候会把容量扩大到原来size的两倍,计算方式为比used*2大的最小的2的n次方.
但是如果dict正在进行bgsave,为了减少内存页的数量,redis会暂缓扩容,但是如果dict中元素的数量继续增加,当到达size的5被的时候,redis就会忽略bgsave进行强制扩容,
dict扩容的时候会提前为ht数的第二个元素提前分配好内存空间,然后逐个对第一个元素中的dictEntry进行rehash转移到第二个元素中,,然后把第一个元素指向第二个元素
4.缩容
当dict中的元素不断被删除的时候,dictht中剩余的空间越来越多,当used的数量小于size的10%的时候就会触发redis的缩容操作,缩容的时候是不会考虑redis是否正在进行bgsave操作的.
dict缩容和扩容一样也会先为tht数组的第二个元素分配内存,然后把第一个元素中的数据rehash然后放到第二个元素中,然后把第一个元素指向第二个元素
5.rehash
因为redis是单线程执行命令的.而如果dict中元素的数量表多,则进行扩容或则缩容的时候就会耗费的时间比较长,这就导致后续的指令都会返回的较慢,redis为了解决这个问题采用饿了渐进式hash的策略.
即再进行rehash的时候不是一次性的把第一个dictht中的元素一次性全部rehash然后放到第二个dictht中,而是每次等到dict中有增删改查操作的时候每次顺便把dictht一种的部分元素转移到dictht2中,这样就把rehash的时间分配到了不同的指令上,不会造成一个指令延迟时间较长的情况.如果正在进行rehash的dict中没有来自客户端的指令,那么redis会定再定时任务中对数据进行主动的rehash操作