Redis当中的hash类似于java当中的HashMap,但也存在着很多的区别。在Redis当中,hash有两种底层实现方式。
本篇博客主要介绍dict的实现方式。
dict的结构定义如下:
typedf struct dict{
dictType *type;//和特定类型键值对相关的函数;
void *privdata;//上述特定函数的可选参数;
dictht ht[2];//两张hash表
int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
unsigned long iterators; //正在迭代的迭代器数量
}dict;
对于字典的结构有了初步印象之后,里面比较关键的dictht hash表的结构是怎样的呢?继续往下看!
hash表的结构定义如下:
typedf struct dictht{
dictEntry **table;//存储数据的数组 二维
unsigned long size;//数组的大小
unsigned long sizemask;//哈希表的大小的掩码,用于计算索引值,总是等于size-1
unsigned long used; 哈希表中中元素个数
}dictht;
二维数组的结构又是如何定义的呢?
上面提到的二维数组dictEntry,是真正的存储key-value键值对的地方,结构定义如下:
typedf struct dictEntry{
void *key;//键
union{
void val;
unit64_t u64;
int64_t s64;
double d;
}v;//值
struct dictEntry *next;//指向下一个节点的指针
}dictEntry;
key表示键,v表示值
next是指向下一个结点的指针,因为这里的hash表是通过拉链法来解决冲突的。
下面是完整的dict字典的结构图
hash表的扩容是为了减少hash冲突的概率,当hash表中的数据逐渐增多的时候,会导致冲突的概率增大,从而导致每个槽位下的链表的长度会变长。那么就会影响到查询的效率了。
而hash表的缩容是为了减少空间的消耗。Redis的数据是保存在内存当中的,若一个hash表占用很大的空间,里面的数据却很少,那么是极度的浪费,所以需要缩容操作。
在Java当中HashMap有负载因子0.75,当hash表当中实际的元素是hash表槽位的0.75倍的时候,会发生扩容。同样的,在Redis当中,也有负载因子的概念,计算公式是hash表中已保存结点的数量/hash表的长度。
也就是上面结构中提到的 factor = ht[0].used / ht[0].size
Redis中,有三条关于扩容和缩容的规则:
而不管缩容还是扩容,大小是有规定的,如下:
扩容:扩容后的dictEntry数组长度为第一个大于等于 ht[0].used * 2 的 2^n
也就是第一个大于等于已使用数量的两倍的2的幂次方。
缩容:缩容后的dictEntry数组长度为第一个大于等于 ht[0].used 的 2^n
也就是第一个大于等于已使用数量的2的幂次方。
第三节介绍了为何需要扩容缩容、扩容缩容的规则以及时机。下面将介绍扩容和缩容的过程,在Redis当中在扩容和缩容的时候,会执行rehash。
对比Java当中的HashMap的rehash,java当中需要新建一个hash表,然后一次性的将旧表里的数据进行rehash到新的hash表当中,之后在释放掉原油的hash表。而这一过程的时间复杂度达到了O(n)。
但是Redis是使用单线程的方式来执行请求命令的,所以无法接受一个时间复杂度为O(n)的操作,所以这就需要渐进式rehash。
渐进式,顾名思义也就是一步一步的进行rehash,将一个完整的rehash的过程给拆成多次取执行。
下面是完整的rehash的过程
想必在这个rehash过程当中,还有很多的疑问,rehash过程中,如何执行的增删改查呢?
上面增删改查的操作,保证了dictht[0]当中的数据只会减少不增加,最终就没有数据了。
渐进式rehash的优缺点
优点:采用了分而治之的思想,将 rehash
操作分散到每一个对该哈希表的操作上以及定时函数上,避免了集中式rehash
带来的性能压力。
缺点:在 rehash 的时间内,需要保存两个 hash 表,对内存的占用稍大,而且如果在 redis 服务器本来内存满了的时候,突然进行 rehash 会造成大量的 key 被抛弃。
hset
使用方法:hset hash field value
将哈希表hash 中键为field的值设置为value
若当前hash表不存在,那么会新建一个哈希表并执行hset操作。返回1
若field键值对已经存在于哈希表中,新的value会覆盖旧值。返回0
hsetnx
使用方法:hsetnx hash field value
和上面的区别在于,仅当field尚未存在于哈希表当中,将它的值设置为value
若给定的field已经存在于哈希表当中,则不进行覆盖,返回0.否则执行设置键值对返回1.
使用方法:hget hash field
返回哈希表hash当中 键为field的值。
不存在返回nil
使用方法:exists hash field
和上面的类似,只是这里是判断哈希表hash中是否存在键field
存在返回1 不存在返回0
使用方法:hdel hash field [field …]
删除哈希表hash当中一个或多个指定键的键值对,不存在的键会被忽略掉。返回删除成功的键值对的数量。
使用方法:hlen hash
返回哈希表hash当中 键值对的数量
使用方法:hstrlen hash field
返回指定的哈希表hash当中的键为field对应的值的长度。
使用方法:hincrby hash field increment
给指定的哈希表hash 中的field键对应的value值加上increment
增加的值可以是负数,相当于执行了减法操作。
若哈希表hash不存在,则会新建一个哈希表并执行命令。
若field不存在,那么在执行命令之前,会讲field对应的值初始化为0,然后执行hincrby
hincrbyfloat
和上面的区别在于,这里是浮点数的变动。
使用方法:hmset hash field value [field value …]
和hset的区别在于,这个指令可以同时讲多个键值对设置到哈希表hash当中。
使用方法:hmget hash field [field …]
返回指定哈希表hash当中键为field的值,可以是多个
若不存在则返回nil
使用方法:hgetall hash
返回哈希表hash中所有的键值对,键在前值在后的输出