1.内部结构
哈希类型的内部编码有两种:ziplist(压缩列表),hashtable(哈希表)。
只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型;
- 哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)
所有值都小于hash-max-ziplist-value配置(默认64字节)
`ziplist`使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比`hashtable`更加优秀。当哈希类型无法满足`ziplist`的条件时,Redis会使用`hashtable`作为哈希的内部实现,因为此时`ziplist`的读写效率会下降,而`hashtable`的读写时间复杂度为O(1)。
1.1 压缩列表(ziplist.c/ziplist.h)
1.2 字典(dict.c/dict.h)
1.2.1 dict 字典
typedef struct dict{
//类型特定函数
void *type;
//私有数据
void *privdata;
//哈希表-见1.2.3
dictht ht[2];
//rehash 索引 当rehash不在进行时 值为-1
int trehashidx;
}dict;
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。
- type属性是一个指向dictType结构的指针,每个dictType用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
- privdata属性则保存了需要传给给那些类型特定函数的可选参数。
- ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表, 一般情况下,字典只使用ht[0] 哈希表, ht[1]哈希表只会对ht[0]哈希表进行rehash时使用。
- rehashidx记录了rehash目前的进度,如果目前没有进行rehash,值为-1。
1.2.2 dictType 多态类型
typedef struct dictType
{
//计算哈希值的函数
unsigned int (*hashFunction) (const void *key);
//复制键的函数
void *(*keyDup) (void *privdata,const void *key);
//复制值的函数
void *(*keyDup) (void *privdata,const void *obj);
//复制值的函数
void *(*keyCompare) (void *privdata,const void *key1, const void *key2);
//销毁键的函数
void (*keyDestructor) (void *privdata, void *key);
//销毁值的函数
void (*keyDestructor) (void *privdata, void *obj);
}dictType;
1.2.3 dictht散列表
类似Java中的HashMap
typedef struct dictht
{
//哈希表数组,C语言中,*号是为了表明该变量为指针,有几个* 号就相当于是几级指针,这里是二级指针,理解为指向指针的指针
dictEntry **table;// 就理解成一个二维的数组,类似于HashMap中的结构
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希已有节点的数量
unsigned long used;
}dictht;
- table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对
- size属性记录了哈希表的大小,也是table数组的大小
- used属性则记录哈希表目前已有节点(键值对)的数量
- sizemask属性的值总是等于 size-1(从0开始),这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面(索引下标值)
1.2.4 dictEntry散列表节点
key属性保存着键值中的键,而v属性则保存着键值对中的值,其中键值(v属性)可以是一个指针,或uint64_t整数,或int64_t整数。 next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,解决键冲突问题。//哈希表节点定义dictEntry结构表示,每个dictEntry结构都保存着一个键值对。
typedef struct dictEntry
{
//键
void *key;
//值
union{
void *val;
uint64_t u64;// 在stdint.h头文件中定义 typedef unsigned long long int uint64_t;
int64_t s64;
}v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
1.3 Redis如何解决Hash冲突
1.3.1 拉链法(链表法)
如图所示,当键k0和k1的经过散列函数得到索引值都为1时,就会使用next指针将两个节点连接起来。而由于节点没有指向链尾的指针,因此新的节点总是插入到链表的头部,排在已有节点的前面。(Redis 单线程,所以不会像HashMap那样因多线程访问 导致JDK1.7中头插法导致的死环)
1.3.2 Redis Rehash
随着操作的进行,散列表中保存的键值对会也会不断地增加或减少,为了保证负载因子维持在一个合理的范围,当散列表内的键值对过多或过少时,内需要定期进行rehash,以提升性能或节省内存。Redis的rehash的步骤如下:
1.为字典的ht[1]散列表分配空间,这个空间的大小取决于要执行的操作以及ht[0]当前包含的键值对数量(即:ht[0].used的属性值)
- 扩展操作:ht[1]的大小为 第一个大于等于ht[0].used*2的 。如:ht[0].used=3则ht[1]的大小为8,ht[0].used=4则ht[1]的大小为8。
- 收缩操作: ht[1]的大小为 第一个大于等于ht[0].used的 。
2.将保存在ht[0]中的键值对重新计算键的散列值和索引值,然后放到ht[1]指定的位置上。
3.将ht[0]包含的所有键值对都迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并创建一个新的ht[1]哈希表为下一次rehash做准备;
1.3.3 哈希表的扩展与收缩
服务器目前没有执行BGSAVE(rdb持久化)命令或者BGREWRITEAOF(AOF文件重写)命令,并且散列表的负载因子大于等于1。
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于5。
当负载因子小于0.1时,程序自动开始执行收缩操作。
其中哈希表的负载因子可以通过公式:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
根据 BGSAVE 命令(RDB持久化命令)或 BGREWRITEAOF (APF持久化命令)命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
1.4 渐进式Rehash
扩展或收缩哈希表需要将 ht[0]
里面的所有键值对 rehash 到 ht[1]
里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
这样做的原因在于, 如果 ht[0]
里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1]
; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1]
的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。
因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]
里面的所有键值对全部 rehash 到 ht[1]
, 而是分多次、渐进式地将 ht[0]
里面的键值对慢慢地 rehash 到 ht[1]
。
1.4.1 渐进式Rehash 步骤
以下是哈希表渐进式 rehash 的详细步骤:
- 为
ht[1]
分配空间, 让字典同时持有ht[0]
和ht[1]
两个哈希表。 - 在字典中维持一个索引计数器变量
rehashidx
, 并将它的值设置为0
, 表示 rehash 工作正式开始。 - 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将
ht[0]
哈希表在rehashidx
索引上的所有键值对 rehash 到ht[1]
, 当 rehash 工作完成之后, 程序将rehashidx
属性的值增一。 - 随着字典操作的不断执行, 最终在某个时间点上,
ht[0]
的所有键值对都会被 rehash 至ht[1]
, 这时程序将rehashidx
属性的值设为-1
, 表示 rehash 操作已完成。
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
1.4.2 完整的渐进式Rehash过程
注意观察在整个 rehash 过程中, 字典的 rehashidx
属性是如何变化的。
1.4.3 渐进式rehash执行期间的哈希表的操作
因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0]
和 ht[1]
两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0]
里面进行查找, 如果没找到的话, 就会继续到 ht[1]
里面进行查找, 诸如此类。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1]
里面, 而 ht[0]
则不再进行任何添加操作: 这一措施保证了 ht[0]
包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
2.常用命令
命令 | 说明 | 时间复杂度 |
---|---|---|
HMSET key field value [field value …] | 设置hash字段值 | O(N) N是设置的字段数 |
HSET key field value | 设置hash里面一个字段的值 | O(1) |
HSETNX key field value | 设置hash的一个字段,只有当这个字段不存在时有效 | O(1) |
HSTRLEN key field | 获取hash里面指定field的长度 | O(1) |
HVALS key | 获得hash的所有值 | O(N) N是Hash的长度 |
HDEL key field [field …] | 删除一个或多个Hash的field | O(N) N是被删除的字段数量。 |
HEXISTS key field | 判断field是否存在于hash中 | O(1) |
HGET key field | 获取hash中field的值 | O(1) |
HGETALL key | 从hash中读取全部的域和值 | O(N) N是Hash的长度 |
HINCRBY key field increment | 将hash中指定域的值增加给定的数字 | O(1) |
HINCRBYFLOAT key field increment | 将hash中指定域的值增加给定的浮点数 | O(1) |
HKEYS key | 获取hash的所有字段 | O(N) N是Hash的长度 |
HLEN key | 获取hash里所有字段的数量 | O(1) |
HMGET key field [field …] | 获取hash里面指定字段的值 | O(N) N是请求的字段数 |
HSCAN key cursor [MATCH pattern] [COUNT count] | 迭代hash里面的元素 |
3.使用场景
3.1 购物车
很多电商网站都会使用 cookie实现购物车,也就是将整个购物车都存储到 cookie里面。这种做法的一大优点:无须对数据库进行写入就可以实现购物车功能,这种方式大大提高了购物车的性能,而缺点则是程序需要重新解析和验证( validate) cookie,确保cookie的格式正确,并且包含的商品都是真正可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连 cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。
购物车的定义非常简单:我们以每个用户的用户ID(或者CookieId)作为Redis的Key,每个用户的购物车都是一个哈希表,这个哈希表存储了商品ID与商品订购数量之间的映射。在商品的订购数量出现变化时,我们操作Redis哈希对购物车进行更新:
如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里面;
用户uid:zhangsan, 商品sku :order_item:skuid:123311223445 ,购买了10个
3.2 计数器
例如某博客 ,blogid为8759 ,在2021年4月16日的访问量的统计。
模拟当年斗鱼 某直播间 online人数 突破12亿的操作,可能就是 redis 的hash value值被直接修改。(^.^)
引用
http://redisbook.com/preview/dict/incremental_rehashing.html
http://zhangtielei.com/posts/blog-redis-dict.html
https://blog.csdn.net/bellediao/article/details/82967645