Redis中Hash(哈希)类型的基本操作

文章目录


一、 哈希简介

几乎所有的主流编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本身又是一个键值对结构,形如 key = "key",value = { { field1, value1 }, ..., {fieldN, valueN } },Redis 键值对和哈希类型二者的关系可以用图 2-15 来表示。

哈希类型中的映射关系通常称为 field-value,用于区分 Redis 整体的键值对(key-value)注意这里的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下文的作用。

二、常用命令

hset

HSET

  • 设置 hash 中指定的字段(field)的值(value)。
  • 语法:HSET key field value [field value ...]
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:插入一组 field 为 O(1), 插入N组 field为 O(N)
  • 返回值:添加字段的个数。
  • 示例:

    此处可以一次只插入一个,也可以一次插入N个。

hget

HGET

  • 获取 hash 中指定字段的值。
  • 语法: HGET key field
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:0(1)
  • 返回值:字段对应的值或者 nil。
  • 示例:

hexists

HEXISTS

  • 判断 hash 中是否有指定的字段。
  • 语法: HEXISTS key field
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:O(1)
  • 返回值:1表示存在,0表示不存在。
  • 示例:

hdel

HDEL

  • 删除 hash 中指定的字段。
  • 语法: HDEL key field [field ....]
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:删除一个元素为 O(1),删除 N 个元素为 O(N).
  • 返回值:本次操作删除的字段个数。
  • 示例:

hkeys

HKEYS

  • 获取 hash 中的所有字段(field)。
  • 语法: HKEYS key
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:O(N),N为field 的个数:
  • 返回值:字段列表。
  • 示例:

hvals

HVALS

  • 获取 hash 中的所有的值(value)。
  • 语法: HVALS key
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:O(N),N为 field 的个数.
  • 返回值:所有的值。
  • 示例:

hgetall

HGETALL

  • 获取 hash 中的所有字段(field)以及对应的值(value)。
  • 语法: HGETALL key
  • 命令有效版本:2.0.0之后
  • 时间复杂度:O(N),N为field 的个数,
  • 返回值:字段和对应的值。
  • 示例:

hmget

HMGET

  • 一次获取 hash 中多个字段的值。
  • 语法: HMGET key field [field ...]
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:只查询一个元素为 0(1),查询多个元素为 O(N),N 为查询元素个数.
  • 返回值:字段对应的值或者 nil。
  • 示例:

在使用 HGETALL 时,如果哈希元素个数比较多,会存在阻塞 Redis 的可能。如果开发人员只需要获取部分 field,可以使用HMGET,如果一定要获取全部 field,可以尝试使用 HSCAN命令,该命令采用渐进式遍历哈希类型,HSCAN 会在后续介绍。

hlen

HLEN

  • 获取 hash 中的所有字段(field)的个数。
  • 语法: HLEN key
  • 命令有效版本:2.0.0 之后
  • 时间复杂度:O(1)
  • 返回值:字段个数。
  • 示例:

hsetnx

HSETNX

  • 在字段不存在的情况下,设置 hash 中的字段和值,如果存在就不会设置。
  • 语法: HSETNX key field value
  • 命令有效版本:2.0.0之后
  • 时间复杂度:0(1)
  • 返回值:1表示设置成功,0 表示失败。
  • 示例:

hincrby

HINCRBY

  • 将 hash 中字段对应的数值添加指定的值。
  • 语法: HINCRBY key field increment
  • 命令有效版本:2.0.0之后
  • 时间复杂度:O(1)
  • 返回值:该字段变化之后的值。
  • 示例:

hincrbyfloat

HINCRBYFLOAT

  • HINCRBY的浮点数版本。
  • 语法: HINCRBYFLOAT key field increment
  • 命令有效版本:2.6.0之后
  • 时间复杂度:0(1)
  • 返回值:该字段变化之后的值。
  • 示例:

hstrlen

HSTRLEN

  • 计算 value 的字符串长度
  • 语法:HSTRLEN key field
  • 返回值:字符串的长度
  • 示例:

三、命令小结

命令 执⾏效果 时间复杂度
hset key field value 设置值 O(1)
hget key field 获取值 O(1)
hdel key field [field...] 删除 field O(k), k 是 field 个数
hlen key 计算 field 个数 O(1)
hgetall key 获取所有的 field-value O(k), k 是 field 个数
hmget 批量获取 field-value O(k), k 是 field 个数
hmset 批量设置 field-value(注意:Redis 3.0.6 后使用 hset 替代) O(k), k 是 field 个数
hexists key field 判断 field 是否存在 O(1)
hkeys key 获取所有的 field O(k), k 是 field 个数
hvals key 获取所有的 value O(k), k 是 field 个数
hsetnx key field value 设置值,但必须在 field 不存在时才能设置成功 O(1)
hincrby key field n 对应 field-value + n(n 为整数) O(1)
hincrbyfloat key field n 对应 field-value + n(n 为浮点数) O(1)
hstrlen key field 计算 value 的字符串长度 O(1)

注意:hmset 命令在 Redis 3.0.6 版本后被废弃,推荐使用 hset 或 hmset 的变种(逐个设置)来替代批量设置的操作。表格中仍列出 hmset 以供参考。

四、哈希内部编码方式

哈希的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)同时所有值都小于 hash-max-ziplist-value配置(默认 64字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable 更加优秀。
  • hashtable(哈希表):当哈希类型无法满足 ziplist的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。下面的示例演示了哈希类型的内部编码,以及响应的变化。
  1. 当 field 个数比较少且没有大的 value 时,内部编码为 ziplist:

listpack 是 Redis 5.0 引入的一个新的内部数据结构,用于替代和优化 ziplist,但在 Redis的官方文档和上下文中,哈希类型仍然使用 ziplist 或 hashtable 作为其内部编码的术语。如果你在使用 Redis 5.0或更高版本,那么哈希类型在内部可能会使用 listpack 来实现 ziplist 的功能,但这一细节通常对开发者是透明的。

  1. 当有 value 大于 64 字节时,内部编码会转换为 hashtable:
  1. 当 field 个数超过 512 时,内部编码也会转换为 hashtable:

由于field个数太多,博主也懒得敲了,感兴趣的自己试试吧。

五、典型应用场景

图 2-16 为关系型数据表记录的两条用户信息,用户的属性表现为表的列,每条用户信息表现为行。如果映射关系表示这两个用户信息,则如图 2-17 所示。

相比于使用 JSON 格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得更灵活。可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性,类似如下伪代码:

cpp 复制代码
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) {
			响应 404
				return null;
		}
	// 将缓存以哈希类型保存
	Redis 执行命令:hmset key name userInfo.name age userInfo.age city userInfo.city
		// 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
		Redis 执行命令:expire key 3600
		// 返回用户信息
		return userInfo;
}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

  1. 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null,如图 2-18所示。
  2. 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等
    基本不可能,维护成本高。

六、 字符串,序列化,哈希对比

截至目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。

  1. 原生字符串类型 ------ 使用字符串类型,每个属性一个键。
cpp 复制代码
set user:1:name James
set user:1:age 23
set user:1:city Beijing

优点:实现简单,针对个别属性变更也很灵活。

缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性。

  1. 序列化字符串类型,例如 JSON 格式
cpp 复制代码
set user:1 经过序列化后的用户对象字符串

优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。

缺点:本身序列化和反序列需要一定开销,同时如果总是操作个别属性则非常不灵活。

  1. 哈希类型
cpp 复制代码
hmset user:1 name James age 23 city Beijing

优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。

缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。

相关推荐
tatasix14 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
秋意钟18 分钟前
缓存雪崩、缓存穿透【Redis】
redis
南城花随雪。27 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了28 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度29 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
简 洁 冬冬31 分钟前
046 购物车
redis·购物车
天海华兮32 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
雯0609~42 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
soulteary1 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
gma9991 小时前
Etcd 框架
数据库·etcd