Hash是redis中常用的一种无序数据结构。结构类似HashMap。
 具体结构如下:key field value
1、优缺点
1.1、优点
- 同类数据归类整合储存,方便数据管理。
 - 相比于string操作消耗内存和CPU更小。
 - 分字段存储,节省网络流量。
 
1.2、缺点
- 过期时间无法设置在field上,只能设置在key上
 - redis集群下不适合大规模使用
 
2、Hash底层结构
2.1、ziplist-压缩列表
2.1.1、使用条件
- 哈希对象存储的键值对个数小于512个
 - 哈希对象存储的键值对的键和值的字符串长度小于64字节
 
2.1.2、数据结构
见list
2.1.3、ziplist的优点
- 为什么不直接使用hashtable?
 
相比于hashtable,ziplist结构少了指针,减少了内存的使用。在redis中内存是非常珍贵的。
- 为什么不使用linkedlist?
 
ziplist存储时内存地址分配是连续,查询更快。
2.2、dict-字典
字典是在hash存储的数据不满足ziplist中的两个任意一个条件时,使用的数据结构。
 由于dict是一种常用的数据结构,但是c语言并不具备此种数据结构,因此redis开发人员自己设计和开发了redis的dict结构。详细结构如下:
typedf struct dict{
    dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储
    void *private;//私有数据
    dictht ht[2];//两张hash表 
    int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
    unsigned long iterators; //正在迭代的迭代器数量
}dict;
 
type和private这两个属性是为了实现字典多态而设置额,当字典中存放着不同类型的值,对应的复制、比较函数也是不一样,这两个字段组合起来可以实现多态的方法调用。ht[2],两个hash表rehashidx,辅助变量,用于记录rehash过程的进度,以及是否正在进行rehash等信息。当此值为-1时表示该dict没有进行rehash操作。iterators,记录此时dict有几个迭代器正在进行遍历过程。
2.2.1、dictht-哈希表
从dict结构上可以看出,dict实际上就是对dictht的操作,dictht的具体结构如下:
typedf struct dictht{
    dictEntry **table;//存储数据的数组 二维
    unsigned long size;//数组的大小
    unsigned long sizemask;//哈希表的大小的掩码,用于计算索引值,总是等于//size-1
    unsigned long used; 哈希表中中元素个数
}dictht;
 
table是一个dictEntry类型的数组,用户真正存储数据。size表示**table这个数组的大小。sizemask用于计算索引的位置,总是等于size-1。used表示**table数组中已有的节点个数。
2.2.2、dictEntry
上面分析dictht实际存储数据的是dictEntry数组,其结构定义如下:
typedf struct dictEntry{
    void *key;//键
    union{
        void val;
        unit64_t u64;
        int64_t s64;
        double d;
    }v;//值
    struct dictEntry *next;//指向下一个节点的指针
}dictEntry;
 
整个dict字典的结构示意图如下:
 
2.2.3、扩容与缩容
当哈希表的数量主键增大时,此时添加数据,产生hash冲突的概率主键增大,且dict也是采用拉链法解决hash冲突的,因此随着hash冲突的增加,链表的长度也在逐渐增大。这时查询的速度会随着链表的长度主键变慢。相反,当元素主键减少时,元素占用dict的空间逐渐减少,处于对内存的极致利用,此时就需要进行缩容操作。
 dict的扩容和缩容操作有一点和Java中的HashMap结构类似,都有负载因子。负载因子一般用于表示集合当前被数据填充的程度。在Redis的字典dict中,负载因子=哈希表已存节点数量/哈希表长度,即:
load factor=ht[0].used/ht[0].size
Redis中,关于扩容和缩容有三条规则:
- 没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的负载因子大于等于1时进行扩容。
 - 正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的负载因子大于等于5时进行扩容。
 - 负载因子小于0.1时,
Redis自动对哈希表进行缩容操作。 
Redis扩容和缩容的数量规则:
- 扩容后:扩容后的
dictEntry数组数量为第一个大于等于ht[0].used*2的2^n; - 缩容后:缩容后的
dictEntry数组数量为第一个大于等于ht[0].used的2^n; 
2.2.4、rehash
Redis的扩容或者缩容,与Java中的HashMap类似都有rehash过程。Java中的HashMap的rehash过程如下:
- 新建一个哈希表,一次性将当前的数据全部
rehash,然后复制到新的哈希表上。 - 舍弃掉原来的哈希表,而持有新的
hash表。这个过程是一个时间复杂度为O(n)的操作。 
对于单线程的Redis而言很难承受这么高的时间复杂度的操作。因此Redis的rehash操作相比较于HashMap有所不同。Redis采用渐进式rehash的方式。其过程如下:
- 假设当前数据在
ht[0]上,那么首先会为ht[1]分配到足够的空间。如果是扩容ht[1]就按照扩容规则进行设置。如果是缩容ht[1]就按照缩容规则设置。 - 在
dict结构中有个rehashidx字段,用来记录rehash的位置。**rehash=0,**表示rehash开始。 rehash进行期间,每次对字典进行添加、删除、查找、更新操作时,除了执行指定的操作外,还会顺带将ht[0]哈希表上的数据rehash到ht[1]上,每rehash一个ht[0]上的数据到ht[1]上rehashidx都会加1。每次顺带的rehash操作只会搬移少量的数据(100个元素)。- 随着字典操作的不断进行,在某个时刻,
ht[0]上的所有数据全部被rehash到ht[1]上,这时rehashidx的值为-1,表示rehash的操作已完成。 
以上就是Redis中的dict的渐进式rehash过程,但是这个过程存在两个问题:
- 在第三步说了,每次在对字典执行增删改查时才会触发
rehash过程。万一某一时间段,一直都没有请求怎么办? 
A:Redis中有一个定时器,会定时去判断rehash是否完成,如果没有完成,则继续进行rehash操作。
- 在
rehash过程中维护两个hash表,是如何对外提供服务的? 
A:对于添加操作,会将数据直接添加到ht[1]上,这样就会保证ht[0]上的数据只会减少不会增加。而对于**删除、更改、查询操作。**会直接在ht[0]上进行操作,尤其这三个操作都会涉及到查询,当在**ht[0]**上查询不到时,会接着去**ht[1]**上查找,如果在找不到,则表示此key不存在。
2.2.5、渐进式rehash的优缺点
- 优点:采用分而治之的思想,将
rehash操作分散到每一个对该哈希表的操作上以及定时函数上,避免了集中式的rehash带来的性能压力。 - 缺点:在
rehash期间内,需要保存两个hash表,对内存的占用稍大,而且如果在redis服务器内存满了的时候,突然进行**rehash**操作,会造成大量key被抛弃。 
2.2.6、思考题
为什么扩容时需要考虑BGSAVE的影响,而缩容时不需要?
BGSAVE时,dict进行扩容,则此时就需要为ht[1]分配内存,若是ht[1]的数据量很大时,就会占用更多的系统内存,造成内存页过多分离,所以为了避免系统耗费更多的开销去回收内存,此时最好不要进行扩容。- 缩容时,结合缩容的条件,此时负载因子<0.1,说明此时的
dict中的数据很少,就算为ht[1]分配内存,也消耗不了多少资源。 



















