hash
- 一. hash 类型介绍
- 二. hash 命令
- hset、hget
- hexists、hdel
- hkeys、hvals、hgetall
- hmset、hmget
- hlen、hstrlen、hsetnx
- hincrby、hincrbyfloat
- 三. hash 命令小结
- 四. hash 内部编码方式
- 五. hash 的应用场景
- 缓存功能
- 缓存方式对比
一. hash 类型介绍
- 哈希表在日常开发中,出场率是非常高的,面试中也是非常重要的考点。
- Redis 自己已经是 “键值对” 结构了,就是通过哈希表的方式来实现的。
- 把 key 这一层组织完成之后,到了 value 这一层,value 的其中一种类型还可以是哈希表。
二. hash 命令
hset、hget
- hset:设置 hash 中指定的一个/多个 字段 (field) 以及对应的值 (value)
- 语法:
hset key field value [field value ...]
- 时间复杂度:插入一组 field和value 为 O(1),插入 N 组 field和value 为 O(N)
- 返回值:添加的字段的个数。
- hget:获取 hash 中指定字段对应的 值 (value)
- 语法:
hget key field
- 时间复杂度:O(1)
- 返回值:字段对应的值或者 nil
hexists、hdel
- hexists:判断 hash 中是否有指定的 字段 (field)
- 语法:
hexists key field
- 时间复杂度:O(1)
- 返回值:1 表示存在,0 表示不存在。
- hdel:删除 hash 中指定的 字段 (field)
- 语法:
hdel key field [field ...]
- 时间复杂度:删除一个元素为 O(1),删除 N 个元素为 O(N)
- 返回值:本次操作删除的字段个数。
这里需要注意的是:
- del 删除的是 key
- hdel 删除的是 field
hkeys、hvals、hgetall
- hkeys:获取 hash 中的所有 字段 (field)
- 语法:
hkeys key
- 时间复杂度:O(N),N 为 字段 (field) 的个数。
- 返回值:所有的 (field)
- hvals:获取 hash 中的所有的字段对应的 值 (value)
- 语法:
hvals key
- 时间复杂度:O(N),N 为 字段 (field) 的个数。
- 返回值:所有的 值(value)
- hgetall:获取 hash 中的所有 字段 (field) 以及对应的 值 (value)
- 语法:
hgetall key
- 时间复杂度:O(N),N 为 字段 (field) 的个数。
- 返回值:字段 (field) 和对应的值 (value)
总结:
- h 系列的命令,必须要保证 key 对应的 value 得是 hash 类型。
- 这两个操作,先根据 key 查找对应的 hash,为 O(1),然后再遍历 hash,为 O(N),N 表示 hash 的元素个数。
- 注意:这些操作也是存在一定的风险的,类似之前介绍的
keys *
,我们也不知道某个 hash 中是否会存在大量的 field,可能导致 Redis 服务器被阻塞。
hmset、hmget
- hmset:设置 hash 中指定的一个/多个 字段 (field) 以及对应的值 (value)
- 语法:
hmset key field value [field value ...]
- 时间复杂度:插入一组 field和value 为 O(1),插入 N 组 field和value 为 O(N)
- 返回值:成功时返回 OK,失败时返回错误。
注意的是:hset 已经支持,在 hash 中插入一组 field和value,所以并不需要使用 hmset,多数情况下,不需要查询所有的 字段 (field),可能只需要查询其中几个 字段 (field),而 hget 一次只能查询一个 字段 (field),于是出现了 hmget
- hmget:一次获取 hash 中多个 字段对应的值 (value)
- 语法:
hmget key field [field ...]
- 时间复杂度:只查询一个元素为 O(1),查询多个元素为 O(N),N 为查询元素个数。
- 返回值:字段对应的值或者 nil
总结:
- 在使用 hkeys、hvals、hgetall 都是存在一定的风险的,如果 hash 元素的个数比较多时,执行的耗时会比较长,从而阻塞 Redis 服务器。
- 一条命令完成所有的遍历操作。
- 如果开发人员只需要获取部分的 字段 (field),可以使用 hmget。
- 一条命令完成所有的遍历操作。
- 如果一定要获取全部 字段 (field),可以尝试使用 hscan 命令,该命令采用 “渐进式遍历” Redis 的 hash 类型。
- 敲一次命令,遍历一小部分。
- 再敲一次命令,再遍历一小部分。
- …
- 连续执行多次,就可以完成整个遍历的过程。
- 化整为零,做到时间可控,不会阻塞 Redis 服务器。
C++ 标准库的设计精妙,实现也是很优雅的,但是 C++ 标准库,横向和其他编程语言比,就是个 弟中弟。
- C++ 标准库给咱们提供的功能太少,提供的各种 “容器”,都是线程不安全的,
- Java 标准库则直接提供了一些线程安全的 “集合类” (Java 中也有 “容器” 这样的术语,但是指的是其他东西)
- 例如:线程安全的哈希表 (ConcurrentHashMap),这个哈希表在扩容的时候,也是按照 “化整为零” 的方式进行的 (数据拷贝的过程化整为零)
hlen、hstrlen、hsetnx
- hlen:获取 hash 中的所有 字段(field) 的个数。
- 语法:
hlen key
- 时间复杂度:O(1),不需要遍历
- 返回值:字段(field) 的个数。
- hstrlen:获取 hash 中字段对应的 值(value) 的长度,单位是字节。
- 语法:
hstrlen key field
- 时间复杂度:O(1)
- 返回值:值(value) 的长度。
- hsetnx:在字段不存在的情况下,设置 hash 中的一个 字段 (field) 以及对应的值 (value)
- 语法:
hsetnx key field value
- 时间复杂度:O(1)
- 返回值:1 表示设置成功,0 表示失败。
hincrby、hincrbyfloat
hash 这里的 value,也可以当做数字来处理,hincrby 就可以加减证书,hincrbyfloat 就可以加减小数,但是使用的频率不是很高,Redis 没有提供类似于 incr 和 decr 的命令。
- hincrby:将 hash 中字段对应的 值 (value) 添加指定的整数值。
- 语法:
hincrby key field increment
- 时间复杂度:O(1)
- 返回值:该字段变化之后的 值(value)
- hincrbyfloat:将 hash 中字段对应的 值 (value) 添加指定的小数值。
- 语法:
hincrbyfloat key field increment
- 时间复杂度:O(1)
- 返回值:该字段变化之后的 值(value)
三. hash 命令小结
命令 | 执行效果 | 时间复杂度 |
---|---|---|
hset key field value [field value …] | 设置 字段(field) 以及对应的 值(value) | O(k),k 示 字段(field) 的个数 |
hsetnx key field value | 不存在字段(field)时,设置 字段(field) 以及对应的 值(value),否则失败 | O(1) |
hget key field | 获取字段对应的 值(value) | O(1) |
hmset key field value [field value …] | 批量设置 字段(field) 以及对应的 值(value) | O(k),k 示 字段(field) 的个数 |
hmget key field [field …] | 批量获取字段对应的 值(value) | O(k),k 示 字段(field) 的个数 |
hdel key field [field …] | 删除指定的 字段(field) | O(k),k 示 字段(field) 的个数 |
hexists key field | 判断 字段(field) 是否存在 | O(1) |
hkeys key | 获取所有的 字段(field) | O(k),k 示 字段(field) 的个数 |
hvals key | 获取所有的字段对应的 值(value) | O(k),k 示 字段(field) 的个数 |
hgetall key | 获取所有 字段(field) 以及对应的 值(value) | O(k),k 示 字段(field) 的个数 |
hincrby key field n | 对应字段的 值(value) 加整数 n | O(1) |
hincrbyfloat key field n | 对应字段的 值(value) 加小数 n | O(1) |
hlen key | 获取 字段(field) 的个数 | O(1) |
hstrlen key field | 获取字段对应的 值(value) 的长度 | O(1) |
- 哈希类型命令的效果、时间复杂度,开发⼈员可以参考此表,结合自身业务需求和数据大小选择合适的命令。
四. hash 内部编码方式
hash 内部有 2 中编码方式:
- ziplist (压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置 (默认 512 个)、同时所有值都小于 hash-max-ziplist-value 配置 (默认 64 字节) 时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
- 这两个配置项,可以到写到
/etc/redis/redis.cof
文件中。
- 这两个配置项,可以到写到
- hashtable (哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)
总结:
- 如果 hash 中,元素的个数较少,使用 ziplist 表示,如果元素的个数比较多,使用 hashtable 表示。
- 如果 hash 中,每个字段对应的 值(value) 的长度都比较短,使用 ziplist 表示,如果某个字段对应的 值(value) 的长度太长,也会转化成 hashtable 表示。
一些具体的压缩算法:rar、zip、gzip、7z。
- 压缩的本质是,针对数据进行重新编码,不同的数据,有不同放入特点,结合这些特点,进行精妙的设计,重新编码之后,就能够缩小体积。
- 例如:字符串 “abbcccddddeeee”,重新编码后就表示为,1a2b3c4d5e,这是比较粗糙的编码方式,事实上上述一些常见的压缩算法都是精妙设计的。
- ziplist 也是同理,内部的数据结构也是精心设计的,目的就是节省内存空间。
- 表示一个普通的 hash 表,可能会浪费一定的内存空间 (hash 首先是一个数组,数组上的有些位置有元素,有些位置没有元素)
- ziplist 付出的代价就是,进行读写元素,速度是比较慢的,如果元素个数少,慢的不明显,如果元素个数太多了,就很慢。
可以通过 object encoding key
来查看具体的编码方式。
五. hash 的应用场景
缓存功能
string 也是可以作为缓存使用的,但是存储结构化数据 (类似:数据库中表这样的结构),使用 hash 类型更合适一些:
- 相比于使用 JSON 格式的 string 缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得更灵活。可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性,类似如下伪代码:
UserInfo getUserInfo(long uid) {// 根据 uid 得到 Redis 的键String key = "user:" + uid;// 尝试从 Redis 中获取对应的值userInfoMap = Redis 执行命令: hgetall key;// 如果缓存命中(hit)if (value != null) {// 将映射关系还原为对象形式UserInfo userInfo = 利用映射关系构建对象(userInfoMap);return userInfo;}// 如果缓存未命中(miss)// 从数据库中,根据 uid 获取用户信息UserInfo userInfo = MySQL 执行 SQL: select * from user_info where uid = <uid>// 如果表中没有 uid 对应的用户信息if (userInfo == null) {响应 404return null;}// 将缓存以哈希类型保存Redis 执行命令: hmset key name userInfo.name age userInfo.age city userInfo.city// 写入缓存,为了防⽌数据腐烂(rot),设置过期时间为1⼩时(3600秒)Redis 执行命令: expire key 3600// 返回用户信息return userInfo;
}
- 如果使用 string (JSON 的格式) 的方式,来表示 USerInfo,万一只想获取某个 字段(field),或者修改某个 字段(field),就需要把整个 JSON 都读出来。解析成对象,操作 字段(field),再重新写成 JSON 字符串,写回过去。
- 如果使用 hash 的方式,来表示 USerInfo,就可以使用 字段(field) 表示对象的每个属性 (数据表的每个列),此时就可以非常方便的获取/修改任何一个属性值了。
- 使用 hash 的方式,确实 读写字段(field) 更直观高效,但是付出的是空间的代价,需要控制 hash 再 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存较大的消耗。
注意:Redis 哈希类型和 MySQL 关系型数据库有两点不同之处:
- 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null,如图下图所示。
- 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。
上述 hash 中,key 中存了 uid,而 field-value 又存了一份 uid,不存 field-value 中的 uid,是否就直接使用 key 中 id 来进行区分,是不是存储空间有一部的节省了呢?
- 如果确实不想存这个 uid 也可以。
- 但是,在工程实践中,一般都会把 uid 在 value 中再存一份,后续写到相关的代码,使用起来会比较方便。
缓存方式对比
- 原生字符串类型:使用字符串类型,每个属性一个键。
- 优点:实现简单,针对个别属性变更也很灵活。
- 缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
- 序列化字符串类型,例如 JSON 格式。
- 优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
- 缺点:本身序列化和反序列需要一定开销,同时如果总是操作个别属性则非常不灵活。
set user:1 经过序列化后的用户对象字符串
- 哈希类型。
- 优点:简单、直观、灵活,尤其是针对信息的局部变更或者获取操作。
- 缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,hashtable 会造成内存的较大消耗。
hmset user:1 name James age 23 city Beijing
解释:高内聚,低耦合
- 高内聚:模块内部的紧密性和单一功能。
- 把有关联的东西放在一起,最好能放在指定的地方。例如:有些人喜欢把脱下来的衣服随意乱扔,床上、沙发上、椅子上,唯独不会出现在衣柜里,这种就是 “低内聚”。
- 低耦合:模块之间关联关系的程度较低。
- 关联关系越大,越容易相互影响,就认为 “高耦合”,有个成语叫 “藕断丝连”。
- 追求 “低耦合”,是为了避免 “牵一发而动全身”,这边改出 bug,一想到了其他地方。
- 使用 string 和 hash 方式,通过 userId,就可以获取用户所有的数据,而使用原生字符串类型,用户不同属性的信息,不是存放在一起的,而是散开的,需要将其全部找出来。