基础命令
-
hset:设置hash中field的值
-
hmset:批量设置hash的field的值
-
hget:获取存储在哈希表中指定字段的值
-
hmget :获取指定字段的值
myRedis:0>hset guohu address yinshanhu "1" myRedis:0>hmset guohu age 28 mobile 183 "OK" myRedis:0>hget guohu age "28" myRedis:0>hmget guohu address mobile 1) "yinshanhu" 2) "183"
-
hdel :删除一个或多个哈希表字段
myRedis:0>hdel guohu mobile "1" myRedis:0>hmget guohu address age mobile 1) "yinshanhu" 2) "31" 3) null
-
hexists :查看哈希表指定的字段是否存在
myRedis:0>hexists guohu address "1" myRedis:0>hexists guohu addresstest "0"
-
hgetall :获取指定key所有的字段值
myRedis:0>hgetall guohu 1) "address" 2) "yinshanhu" 3) "age" 4) "28" 5) "mobile" 6) "183"
-
hkeys:获取所有的key
myRedis:0>hkeys guohu 1) "address" 2) "age" 3) "mobile"
-
hlen : 获取hash的字段的数量
myRedis:0>hlen guohu "3"
-
hincrby:指定字段的整数值加上增量
myRedis:0>hincrby guohu age 1 "29"
-
hincrbyfloat:指定字段的浮点数值加上增量
myRedis:0>hset order price 100.5 "1" myRedis:0>hincrbyfloat order price 5.5 "106" myRedis:0>hincrbyfloat order price -3.5 "102.5"
-
del : 删除key
myRedis:0>del guohu "1" myRedis:0>hmget guohu address age mobile 1) null 2) null 3) null
基础概念图
优缺点分析
- 优势
- 对key进行了聚合
- 避免了key的冲突
- 批量获取值更方便
- 缺点
- 无法对field设置过期时间
- 没有位操作
- key的数据可能会出现分布不均衡
编码选取
hash有两种编码:ziplist和hashtable
编码之间的转换条件如下:
- 键值对的总长度小于64字节(包含键和值):hash-max-ziplist-value控制
- 键值对数量小于512个:hash-max-ziplist-entries控制
以上条件,同时满足则用ziplist存储,否则用hashtable存储
ziplist
ziplist在上一篇list中已经详细介绍过(Redis-List篇),这里只简单介绍下hash对象中的ziplist和list中的区别点
- hash中的ziplist的entry存储的不是单value,而是key-value
- 存储过程中,每个键和每个值都是字符串对象,保存的时候,先保存键的字符串对象,在保存值的字符串对象
hashtable
源码解析
// 源码位置:dict.h , dict.c
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
解析:
redis中键值对存在dicEntry里面,在hash源码中可以看到,针对dicEntry数组做了一层封装,组装成了dictht结构,然后再对2个dictht对象再做一次封装,组成了dict对象
存储结构图
解决键冲突问题
-
hash碰撞
- 将一个新的键值对添加到字典里,需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值将包含新的键值对的哈希节点放到哈希数组的指定索引上,如果两个或者多个键值对的计算结果是一样的,那么就会出现键冲突问题(hash碰撞), hash中采用拉链法解决键冲突问题
-
开放寻址法
- 连续的检查散列表的各项,直到有空位子可以插入位置
- 探测方式包含三种(这里不做详细介绍)
- 线性探测再散列
- 二次探测再散列
- 伪随机再散列
-
链地址法(拉链法)
- 在产生hash碰撞的时候,把碰撞的元素组成一个列表存储再同一个位置上(如下图)
- 在产生hash碰撞的时候,把碰撞的元素组成一个列表存储再同一个位置上(如下图)
rehash(重新散列)
- 负载因子:负载因子=哈希表已使用节点数/哈希表大小
- load_factor = ht[0].used / ht[0].size
- 为什么要重新散列:在不断操作hash表的键值对时,键值对会不断的增多或者减少,而为了让负载因子维持在一个合理的范围就需要对hash进行扩展和收缩,这个操作就是通过rehash(重新散列)来完成的
- hash的使用规则:由之前的结构图可以看到,redis的hash结构,定义了两个哈希表ht[0]和ht[1],默认用ht[0],并且不会为ht[1]分配空间,发生hash碰撞的时候,同一个索引下有多个dictEntry,形成一个dictEntry的链表,而链表越长会导致性能越差,当达到临界条件时,会执行rehash操作
- 临界条件
- 扩容 (满足以下任一条件,即可进行扩容)
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
- 服务端目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于或等于5
- 缩容
- 负载因子小于等于0.1
- 扩容 (满足以下任一条件,即可进行扩容)
扩展:
BGSAVE : Redis-RDB持久化的一个操作,用户后台异步保存当前数据库的数据到磁盘
BGREWRITEAOF:Redis-AOF持久化的一个操作:异步执行一个 AOF文件重写操作
- 操作步骤
- 给dict的ht[1]哈希表分配内存(初始并不分配ht[1]的内存)其大小取决于ht[0].used
- 扩容:2^n中第一个大于等于ht[0].used * 2的值,例如used=3,则扩容后的长度为:2^2 < 3*2 < 2^3 =>8(这里故意用3是防止理解成翻倍扩容)
- 缩容:2^n中第一个大于等于ht[0].used的值(注意没有乘以2)
- 设置字典的属性rehashidx为0,表示正在执行rehash操作
- 将ht[0]中所有的键值对依次重新计算hash值,并放到ht[1]的位置上去,每放完一个rehashidx累加1
- 迁移完毕之后,释放ht[0],并将ht[1]修改成ht[0],然后再创建一个ht[1]的哈希表,为下次rehash做准备
- 修改字典的属性rehashidx为-1,标记此次rehash结束
- 给dict的ht[1]哈希表分配内存(初始并不分配ht[1]的内存)其大小取决于ht[0].used
图解rehash过程
-
初始结构化图解
-
步骤一:分配ht[1]的空间并且修改标记,扩容为例,空间2^n最小值增长
-
步骤二:迁移数据
-
步骤三:释放ht[0],修改标记
渐进式rehash
按照以上说的重新散列逻辑,假设hash表里面的键值对有成千上万个,甚至于十万级别,重新散列会很影响比较损耗性能和可用性,会导致服务器在数据迁移过程中不可用,所以,在rehash的过程并不是一次性的,是分多次渐进式的完成,在渐进式rehash期间,hash的查找操作会在两个hash表上进行,先去查找ht[0],如果没有再去查找ht[1],但是新增操作只会在ht[1]上进行,保证rehash期间ht[0]的键值对只减不增,迁移完毕之后,释放ht[0],而ht[1]会被设置成ht[0],并在新创建一个空白的哈希表,为下次rehash做准备
结尾语
实际工作上,hash的用途还是很广泛的,用于存储一系列的信息还是很方便的,以上文章中多以图解的方式来剖析存储结构,一定程度上避免直接翻阅源码的枯燥,接下来要介绍的是Redis的集合类