前言

Redis 是一个键值对数据库,其键是通过哈希进行存储的。整个 Redis 可以认为是一个外层哈希,之所以称为外层哈希,是因为 Redis 内部也提供了一种哈希类型,这个可以称之为内部哈希。当我们采用哈希对象进行数据存储时,对整个 Redis 而言,就经过了两层哈希存储。

哈希对象

哈希对象本身也是一个 key-value 存储结构,底层的存储结构也可以分为两种:ziplist(压缩列表) 和 hashtable(哈希表)。这两种存储结构也是通过编码来进行区分:

编码属性 描述 object encoding命令返回值
OBJ_ENCODING_ZIPLIST 使用压缩列表实现哈希对象 ziplist
OBJ_ENCODING_HT 使用字典实现哈希对象 hashtable

hashtable

Redis 中的 key-value 是通过 dictEntry 对象进行包装的,而哈希表就是将 dictEntry 对象又进行了再一次的包装得到的,这就是哈希表对象 dictht

typedef struct dictht {
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//掩码大小,用于计算索引值,总是等于size-1
unsigned long used;//哈希表中的已有节点数
} dictht;

注意:上面结构定义中的 table 是一个数组,其每个元素都是一个 dictEntry 对象。

字典

字典,又称为符号表(symbol table),关联数组(associative array)或者映射(map),字典的内部嵌套了哈希表 dictht 对象,下面就是一个字典 ht 的定义:

typedef struct dict {
dictType *type;//字典类型的一些特定函数
void *privdata;//私有数据,type中的特定函数可能需要用到
dictht ht[2];//哈希表(注意这里有2个哈希表)
long rehashidx; //rehash索引,不在rehash时,值为-1
unsigned long iterators; //正在使用的迭代器数量
} dict;

其中 dictType 内部定义了一些常用函数,其数据结构定义如下:

typedef struct dictType {
uint64_t (*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;

当我们创建一个哈希对象时,可以得到如下简图(部分属性被省略):

rehash 操作

dict 中定义了一个数组 ht[2]ht[2] 中定义了两个哈希表:ht[0]ht[1]。而 Redis 在默认情况下只会使用 ht[0],并不会使用 ht[1],也不会为 ht[1] 初始化分配空间。

当设置一个哈希对象时,具体会落到哈希数组(上图中的 dictEntry[3])中的哪个下标,是通过计算哈希值来确定的。如果发生哈希碰撞(计算得到的哈希值一致),那么同一个下标就会有多个 dictEntry,从而形成一个链表(上图中最右边指向 NULL 的位置),不过需要注意的是最后插入元素的总是落在链表的最前面(即发生哈希冲突时,总是将节点往链表的头部放)。

当读取数据的时候遇到一个节点有多个元素,就需要遍历链表,故链表越长,性能越差。为了保证哈希表的性能,需要在满足以下两个条件中的一个时,对哈希表进行 rehash(重新散列)操作:

  • 负载因子大于等于 1dict_can_resize1 时。
  • 负载因子大于等于安全阈值(dict_force_resize_ratio=5)时。

PS:负载因子 = 哈希表已使用节点数 / 哈希表大小(即:h[0].used/h[0].size)。

rehash 步骤

扩展哈希和收缩哈希都是通过执行 rehash 来完成,这其中就涉及到了空间的分配和释放,主要经过以下五步:

  1. 为字典 dictht[1] 哈希表分配空间,其大小取决于当前哈希表已保存节点数(即:ht[0].used):

    • 如果是扩展操作则 ht[1] 的大小为 2 的 n次方中第一个大于等于ht[0].used * 2属性的值(比如used=3,此时ht[0].used * 2=6,故 23次方为8就是第一个大于used * 2 的值(2 的 2 次方 < 6 且 2 的 3 次方 > 6))。
    • 如果是收缩操作则 ht[1] 大小为 2 的 n 次方中第一个大于等于 ht[0].used 的值。
  2. 将字典中的属性 rehashix 的值设置为 0,表示正在执行 rehash 操作。

  3. ht[0] 中所有的键值对依次重新计算哈希值,并放到 ht[1] 数组对应位置,每完成一个键值对的 rehash之后 rehashix 的值需要自增 1

  4. ht[0] 中所有的键值对都迁移到 ht[1] 之后,释放 ht[0] ,并将 ht[1] 修改为 ht[0],然后再创建一个新的 ht[1] 数组,为下一次 rehash 做准备。

  5. 将字典中的属性 rehashix 设置为 -1,表示此次 rehash 操作结束,等待下一次 rehash

渐进式 rehash

Redis 中的这种重新哈希的操作因为不是一次性全部 rehash,而是分多次来慢慢的将 ht[0] 中的键值对 rehashht[1],故而这种操作也称之为渐进式 rehash。渐进式 rehash 可以避免集中式 rehash 带来的庞大计算量,是一种分而治之的思想。

在渐进式 rehash 过程中,因为还可能会有新的键值对存进来,此时** Redis 的做法是新添加的键值对统一放入 ht[1] 中,这样就确保了 ht[0] 键值对的数量只会减少**。

当正在执行 rehash操作时,如果服务器收到来自客户端的命令请求操作,则会先查询 ht[0],查找不到结果再到ht[1] 中查询

ziplist

关于 ziplist 的一些特性,之前的文章中有单独进行过分析,想要详细了解的,可以点击这里。但是需要注意的是哈希对象中的 ziplist 和列表对象中 ziplist 的有一点不同就是哈希对象是一个 key-value 形式,所以其 ziplist 中也表现为 key-valuekeyvalue 紧挨在一起:

ziplist 和 hashtable 的编码转换

当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用 ziplist 编码来进行存储:

  • 哈希对象中的所有键值对总长度(包括键和值)小于等于 64字节(这个阈值可以通过参数 hash-max-ziplist-value 来进行控制)。
  • 哈希对象中的键值对数量小于等于 512 个(这个阈值可以通过参数 hash-max-ziplist-entries 来进行控制)。

一旦不满足这两个条件中的任意一个,哈希对象就会选择使用 hashtable 编码进行存储。

哈希对象常用命令

  • hset key field value:设置单个 field(哈希对象的 key 值)。
  • hmset key field1 value1 field2 value2 :设置多个 field(哈希对象的 key 值)。
  • hsetnx key field value:将哈希表 key 中域 field 的值设置为 value,如果 field 已存在,则不执行任何操作。
  • hget key field:获取哈希表 key 中的域 field 对应的 value
  • hmget key field1 field2:获取哈希表 key 中的多个域 field 对应的 value
  • hdel key field1 field2:删除哈希表 key 中的一个或者多个 field
  • hlen key:返回哈希表key中域的数量。
  • hincrby key field increment:为哈希表 key 中的域 field 的值加上增量 incrementincrement 可以为负数,如果 field 不是数字则会报错。
  • hincrbyfloat key field increment:为哈希表 key 中的域 field 的值加上增量 incrementincrement 可以为负数,如果 field 不是 float 类型则会报错。
  • hkeys key:获取哈希表 key 中的所有域。
  • hvals key:获取哈希表中所有域的值。

了解了操作哈希对象的常用命令,我们就可以来验证下前面提到的哈希对象的类型和编码了,在测试之前为了防止其他 key 值的干扰,我们先执行 flushall 命令清空 Redis 数据库。

然后依次执行如下命令:

hset address country china
type address
object encoding address

得到如下效果:

可以看到当我们的哈希对象中只有一个键值对的时候,底层编码是 ziplist

现在我们将 hash-max-ziplist-entries 参数改成 2,然后重启 Redis,最后再输入如下命令进行测试:

hmset key field1 value1 field2 value2 field3 value3
object encoding key

输出之后得到如下结果:

可以看到,编码已经变成了 hashtable

总结

本文主要介绍了 Redis5 种常用数据类型中的哈希类型底层的存储结构 hashtable 的使用,以及当 hash 分布不均匀时候 Redis 是如何进行重新哈希的问题,最后了解了哈希对象的一些常用命令,并通过一些例子验证了本文的结论。

Redis中哈希分布不均匀该怎么办的更多相关文章

  1. Redis中的哈希(Hash)

    Redis 哈希(Hash) Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象. Redis 中每个 hash 可以存储 232 - 1 键值 ...

  2. redis中的key设置过期时间

    EXPIRE key seconds 为给定  key  设置生存时间,当  key  过期时(生存时间为  0  ),它会被自动删除. 在 Redis 中,带有生存时间的  key  被称为『易失的 ...

  3. 关于Redis中的数据类型

    一. Redis常用数据类型 Redis最为常用的数据类型主要有以下: String Hash List Set Sorted set 一张图说明问题的本质 图一: 图二: 代码: /* Object ...

  4. 快速同步mysql数据到redis中

    MYSQL快速同步数据到Redis 举例场景:存储游戏玩家的任务数据,游戏服务器启动时将mysql中玩家的数据同步到redis中. 从MySQL中将数据导入到Redis的Hash结构中.当然,最直接的 ...

  5. 超大批量删除redis中无用key+配置

    目前线上一个单实例redis中无用的key太多,决定删除一部分. 1.删除指定用户的key,使用redis的pipeline 根据一定条件把需要删除的用户统计出来,放到一个表里面,表为 del_use ...

  6. Redis中的数据对象

    redis对象 redis中有五种常用对象 我们所说的对象的类型大多是值的类型,键的类型大多是字符串对象,值得类型大概有以下几种,但是无论哪种都是基于redisObject实现的 redisObjec ...

  7. Redis中的基本数据结构

    Redis基础数据结构 基础数据结构 sds简单动态字符串 数据结构 typedef struct sdstr{ int len // 字符串分配的字节 int free // 未使用的字节数 cha ...

  8. Redis 中的数据类型及基本操作

    Redis 内置的数据类型有 5种:字符串String.哈希Hash.列表List.集合Set.有序集合ZSet 字符串类型 String 是 Redis 中最基本的类型,一个 key 对应着一个 v ...

  9. redis-cli 通过管道 --pipe 快速导入数据到redis中

    最近有个需求,需要把五千万条数据批量写入redis中,方法倒是有很多种!效率最高的就是通过redis-cl管道的方式写入 一:先看看命令 cat redis.txt | redis-cli -h 12 ...

  10. redis中各种数据类型的常用操作方法汇总

    在spring中使用jedisTemplate操作,详见https://www.cnblogs.com/EasonJim/p/7803067.html 一.Redis的五大数据类型 1.String( ...

随机推荐

  1. 『SQL注入』 User-Agent 手工注入的探测与利用分析

    原理很简单:后台在接收UA时没有对UA做过滤,也没有PDO进行数据交互(实际PDO是非常有必要的),导致UA中有恶意代码,最终在数据库中执行. Bug 代码: 本地顺手打了一个环境,Bug 代码部分: ...

  2. Android_Studio 及SDK下载

    Android Studio includes all the tools you need to build apps for Android. DOWNLOAD ANDROID STUDIO 2. ...

  3. haproxy配置监控redis主备切换(转)

    环境前提:     redis sentinel配置,三台主机,且配置运行良好        配置文件中添加: frontend ft_redis  bind 0.0.0.0:6379 name re ...

  4. 迄今为止 .Net 平台功能最强大,性能最佳的 JSON 序列化和反序列化库。

    Swifter.Json 这是迄今为止 .Net 平台功能最强大,性能最佳的 JSON 序列化和反序列化库. Github : https://github.com/Dogwei/Swifter.Js ...

  5. Spring Cloud Turbine微服务集群实时监控

    本文代码下载地址: https://gitlab.com/mySpringCloud/turbine SpringBoot版本:1.5.9.RELEASE (稳定版) SpringCloud版本:Ed ...

  6. MySQL 数据库 简单操作命令 (部分总结)

    1.查看进程方式查看数据库 ps - ajx|grep mysql 2.登录 MySQL mysql -u用户名 -p密码 3.开启服务 sudo service mysql start 4.停止服务 ...

  7. Jmeter(二十四)Jmeter-Question之“加密请求参数”

    日常接口测试碰到参数加密的情况不在少数,当然与之相对的也有解密.直接记录实例: 排除各家用的不一样的加密方式,用的最多的还是MD5加密(16,32).Jmeter3.2版本已经有解决方案 1.${__ ...

  8. orm 练习题

    一: 多表练习查询 1. 自行创建测试数据: 2. 查询学生总人数: 3. 查询“生物”课程和“物理”课程成绩都及格的学生id和姓名: 4. 查询每个年级的班级数,取出班级数最多的前三个年级: 5. ...

  9. UVA.10474 Where is the Marble ( 排序 二分查找 )

    UVA.10474 Where is the Marble ( 排序 二分查找 ) 题意分析 大水题一道.排序好找到第一个目标数字的位置,返回其下标即可.暴力可过,强行写了一发BS,发现错误百出.应了 ...

  10. 使用Mybatis时报错Invalid bound statement (not found):

    使用逆向工程时生成的.xml文件在conf目录下,而使用查询方法时,无法在dao包下找到xml文件,所以报错. 测试代码如下所示: @Test public void testSimple() thr ...