2.4 哈希 (Hash) Redis 数据类型详解:2.4 哈希 (Hash) 的实践与应用 2.4 哈希 (Hash) 详解 2.4.1 哈希 (Hash) 的基本概念 Redis 哈希本质上是一个字符串字段 (field) 和字符串值 (value) 之间的映射表。可以将它类比为编程语言中的字典 (Dictionary) 或哈希表 (Hash Table)。 每个哈希都由一个唯一的键 (Key) 标识,这个键指向一个包含多个字段值对的哈希结构。 关键特性: 键值对集合: 哈希存储的是字段 (field) 和值 (value) 的集合,每个字段在其哈希中都是唯一的。 字符串类型: 哈希的字段和值都是字符串类型。
2.4 哈希 (Hash) 详解
Redis 哈希本质上是一个字符串字段 (field) 和字符串值 (value) 之间的映射表。可以将它类比为编程语言中的字典 (Dictionary) 或哈希表 (Hash Table)。 每个哈希都由一个唯一的键 (Key) 标识,这个键指向一个包含多个字段值对的哈希结构。
关键特性:
键值对集合: 哈希存储的是字段 (field) 和值 (value) 的集合,每个字段在其哈希中都是唯一的。
字符串类型: 哈希的字段和值都是字符串类型。
高效访问: Redis 针对哈希操作进行了优化,可以高效地进行字段的读取、写入和删除操作。
节省内存: 相比于将每个字段值对都存储为独立的键值对,使用哈希可以将相关数据聚合在一起,在某些情况下可以更有效地利用内存。
适用场景:
对象存储: 非常适合存储对象数据,例如用户信息、商品信息等。对象的属性可以映射为哈希的字段,属性值则映射为哈希的值。
缓存: 可以缓存结构化的数据,例如 API 返回的 JSON 数据,可以将 JSON 对象的字段作为哈希的字段进行存储。
计数器: 可以利用哈希的字段作为计数器的名称,值作为计数器的值,实现细粒度的计数功能。
会话管理: 可以存储用户的会话信息,例如用户 ID、登录状态、权限信息等。
社交关系: 可以存储用户的关注列表、粉丝列表等,字段可以为用户 ID,值可以为关注/粉丝关系的状态。
接下来,我们将通过实际的代码示例,详细介绍 Redis 哈希的常用命令。我们将使用 Redis 官方推荐的 Python 客户端库 redis-py 进行演示。 如果你还没有安装 redis-py,可以使用 pip 进行安装:
pip install redis
连接 Redis
首先,我们需要连接到 Redis 服务器。假设你的 Redis 服务器运行在本地默认端口 6379,可以使用以下代码进行连接:
import redis # 连接 Redis r = redis.Redis(host='localhost', port=6379, db=0) # 测试连接 try: r.ping() print("成功连接到 Redis 服务器!") except redis.exceptions.ConnectionError as e: print(f"连接 Redis 服务器失败: {e}") exit()
2.4.2.1 HSET - 设置哈希字段的值
HSET key field value 命令用于设置哈希 key 中字段 field 的值为 value。
如果字段不存在: 则创建该字段并设置值。
如果字段已存在: 则覆盖原有值。
代码示例:
# 设置哈希字段值 r.hset("user:1001", "name", "张三") r.hset("user:1001", "age", 30) r.hset("user:1001", "city", "北京") print("哈希字段设置成功!")
详解:
r.hset("user:1001", "name", "张三"): 这行代码将键名为 "user:1001" 的哈希对象中,字段 "name" 的值设置为 "张三"。
类似地,后续两行代码分别设置了字段 "age" 和 "city" 的值。
2.4.2.2 HGET - 获取哈希字段的值
HGET key field 命令用于获取哈希 key 中字段 field 的值。
如果字段存在: 返回字段的值。
如果字段不存在: 返回 None (在 redis-py 中) 或 nil (在 Redis 客户端中)。
代码示例:
# 获取哈希字段值 name = r.hget("user:1001", "name") age = r.hget("user:1001", "age") city = r.hget("user:1001", "city") gender = r.hget("user:1001", "gender") # 不存在的字段 print(f"姓名: {name.decode('utf-8') if name else 'N/A'}") # 需要解码字节串 print(f"年龄: {age.decode('utf-8') if age else 'N/A'}") print(f"城市: {city.decode('utf-8') if city else 'N/A'}") print(f"性别: {gender}") # 不存在的字段返回 None
详解:
r.hget("user:1001", "name"): 获取哈希 "user:1001" 中字段 "name" 的值。
name.decode('utf-8') if name else 'N/A': r.hget 返回的是字节串 (bytes) 类型,需要使用 decode('utf-8') 将其解码为字符串。如果字段不存在,r.hget 返回 None,我们使用条件表达式 if name else 'N/A' 来处理字段不存在的情况,并显示 "N/A"。
2.4.2.3 HDEL - 删除哈希字段
HDEL key field [field ...] 命令用于删除哈希 key 中的一个或多个字段。
删除成功: 返回被成功删除的字段数量。
如果字段不存在: 会被忽略,不会报错,但也不会被计数到删除数量中。
代码示例:
# 删除哈希字段 deleted_count = r.hdel("user:1001", "city", "gender") # 删除 city 和 gender 字段 print(f"成功删除 {deleted_count} 个字段") # 再次获取字段值,验证删除结果 city = r.hget("user:1001", "city") gender = r.hget("user:1001", "gender") print(f"城市: {city}") # None print(f"性别: {gender}") # None
详解:
r.hdel("user:1001", "city", "gender"): 尝试删除哈希 "user:1001" 中的 "city" 和 "gender" 字段。由于 "gender" 字段本来就不存在,所以只删除了 "city" 字段。
deleted_count 变量接收返回值,表示成功删除的字段数量。
后续的 r.hget 操作验证了 "city" 和 "gender" 字段已被删除 (返回 None)。
2.4.2.4 HEXISTS - 检查哈希字段是否存在
HEXISTS key field 命令用于检查哈希 key 中字段 field 是否存在。
字段存在: 返回 1。
字段不存在或哈希不存在: 返回 0。
代码示例:
# 检查哈希字段是否存在 exists_name = r.hexists("user:1001", "name") exists_city = r.hexists("user:1001", "city") print(f"字段 'name' 是否存在: {exists_name}") # 1 (True) print(f"字段 'city' 是否存在: {exists_city}") # 0 (False)
详解:
r.hexists("user:1001", "name"): 检查哈希 "user:1001" 中是否存在字段 "name"。由于之前我们设置过 "name" 字段,所以返回 1 (表示存在)。
r.hexists("user:1001", "city"): 检查字段 "city" 是否存在。由于之前我们删除了 "city" 字段,所以返回 0 (表示不存在)。
2.4.2.5 HLEN - 获取哈希字段的数量
HLEN key 命令用于获取哈希 key 中字段的数量。
哈希存在: 返回字段数量。
哈希不存在: 返回 0。
代码示例:
# 获取哈希字段数量 field_count = r.hlen("user:1001") print(f"哈希 'user:1001' 的字段数量: {field_count}") # 2 (name, age) field_count_non_existent = r.hlen("user:1002") # 不存在的哈希 print(f"哈希 'user:1002' 的字段数量: {field_count_non_existent}") # 0
详解:
r.hlen("user:1001"): 获取哈希 "user:1001" 的字段数量,当前哈希包含 "name" 和 "age" 两个字段,所以返回 2。
r.hlen("user:1002"): 获取不存在的哈希 "user:1002" 的字段数量,返回 0。
2.4.2.6 HGETALL - 获取哈希中所有的字段和值
HGETALL key 命令用于获取哈希 key 中所有的字段和值。
哈希存在: 返回一个包含所有字段和值的字典。
哈希不存在: 返回一个空字典。
代码示例:
# 获取哈希中所有的字段和值 all_fields_values = r.hgetall("user:1001") print(f"哈希 'user:1001' 的所有字段和值: {all_fields_values}") # 输出: {b'name': b'\xe5\xbc\xa0\xe4\xb8\x89', b'age': b'30'} # 遍历字典并解码 for field_bytes, value_bytes in all_fields_values.items(): field = field_bytes.decode('utf-8') value = value_bytes.decode('utf-8') print(f"字段: {field}, 值: {value}") all_fields_values_non_existent = r.hgetall("user:1002") # 不存在的哈希 print(f"哈希 'user:1002' 的所有字段和值: {all_fields_values_non_existent}") # {} (空字典)
详解:
r.hgetall("user:1001"): 获取哈希 "user:1001" 的所有字段和值,返回一个字典。字典的键和值都是字节串类型。
遍历字典并解码: 我们使用 for 循环遍历返回的字典,并使用 decode('utf-8') 将键和值都解码为字符串进行打印。
r.hgetall("user:1002"): 获取不存在的哈希 "user:1002" 的所有字段和值,返回一个空字典。
2.4.2.7 HKEYS - 获取哈希中所有的字段
HKEYS key 命令用于获取哈希 key 中所有的字段。
哈希存在: 返回一个包含所有字段的列表。
哈希不存在: 返回一个空列表。
代码示例:
# 获取哈希中所有的字段 fields = r.hkeys("user:1001") print(f"哈希 'user:1001' 的所有字段: {fields}") # 输出: [b'name', b'age'] # 遍历列表并解码 for field_bytes in fields: field = field_bytes.decode('utf-8') print(f"字段: {field}") fields_non_existent = r.hkeys("user:1002") # 不存在的哈希 print(f"哈希 'user:1002' 的所有字段: {fields_non_existent}") # [] (空列表)
详解:
r.hkeys("user:1001"): 获取哈希 "user:1001" 的所有字段,返回一个列表,列表中的元素是字节串类型的字段名。
遍历列表并解码: 我们使用 for 循环遍历列表,并使用 decode('utf-8') 将字段名解码为字符串进行打印。
r.hkeys("user:1002"): 获取不存在的哈希 "user:1002" 的所有字段,返回一个空列表。
2.4.2.8 HVALS - 获取哈希中所有的值
HVALS key 命令用于获取哈希 key 中所有的值。
哈希存在: 返回一个包含所有值的列表。
哈希不存在: 返回一个空列表。
代码示例:
# 获取哈希中所有的值 values = r.hvals("user:1001") print(f"哈希 'user:1001' 的所有值: {values}") # 输出: [b'\xe5\xbc\xa0\xe4\xb8\x89', b'30'] # 遍历列表并解码 for value_bytes in values: value = value_bytes.decode('utf-8') print(f"值: {value}") values_non_existent = r.hvals("user:1002") # 不存在的哈希 print(f"哈希 'user:1002' 的所有值: {values_non_existent}") # [] (空列表)
详解:
r.hvals("user:1001"): 获取哈希 "user:1001" 的所有值,返回一个列表,列表中的元素是字节串类型的值。
遍历列表并解码: 我们使用 for 循环遍历列表,并使用 decode('utf-8') 将值解码为字符串进行打印。
r.hvals("user:1002"): 获取不存在的哈希 "user:1002" 的所有值,返回一个空列表。
2.4.2.9 HMGET - 批量获取哈希字段的值
HMGET key field [field ...] 命令用于批量获取哈希 key 中指定字段的值。
返回一个列表: 列表中元素的顺序与请求字段的顺序一致。
字段存在: 列表中对应位置的元素为字段的值。
字段不存在: 列表中对应位置的元素为 None (在 redis-py 中) 或 nil (在 Redis 客户端中)。
代码示例:
# 批量获取哈希字段的值 field_values = r.hmget("user:1001", "name", "age", "city", "gender") print(f"批量获取字段值: {field_values}") # 输出: [b'\xe5\xbc\xa0\xe4\xb8\x89', b'30', None, None] # 遍历列表并解码 fields_to_get = ["name", "age", "city", "gender"] for i, value_bytes in enumerate(field_values): field_name = fields_to_get[i] value = value_bytes.decode('utf-8') if value_bytes else 'N/A' print(f"字段: {field_name}, 值: {value}")
详解:
r.hmget("user:1001", "name", "age", "city", "gender"): 批量获取哈希 "user:1001" 中 "name", "age", "city", "gender" 字段的值。由于 "city" 和 "gender" 字段不存在,所以返回列表中对应位置的元素为 None。
遍历列表并解码: 我们使用 enumerate 获取列表的索引和值,并结合 fields_to_get 列表来获取字段名,然后解码值并打印。
2.4.2.10 HMSET - 批量设置哈希字段的值 (已过时)
HMSET key field value [field value ...] 命令用于批量设置哈希 key 中多个字段的值。 在 Redis 4.0.0 版本之后,HMSET 命令被标记为过时 (deprecated),推荐使用 HSET 命令代替。
虽然 HMSET 已经过时,但为了完整性,我们仍然简单介绍一下其用法。
代码示例 (不推荐使用):
# 批量设置哈希字段值 (不推荐使用 HMSET) # r.hmset("user:1002", {"name": "李四", "age": 25, "city": "上海"}) # 旧版本 redis-py 用法 r.hset("user:1002", mapping={"name": "李四", "age": 25, "city": "上海"}) # 新版本 redis-py 推荐用法 print("批量哈希字段设置成功!") all_fields_values_user2 = r.hgetall("user:1002") print(f"哈希 'user:1002' 的所有字段和值: {all_fields_values_user2}")
详解:
新版本 redis-py 使用 hset 和 mapping 参数来替代 hmset。 r.hset("user:1002", mapping={"name": "李四", "age": 25, "city": "上海"}) 使用 hset 命令,并通过 mapping 参数传递一个字典,批量设置多个字段值。
r.hgetall("user:1002") 验证了字段批量设置成功。
2.4.2.11 HINCRBY - 增加哈希字段的数值
HINCRBY key field increment 命令用于将哈希 key 中字段 field 的数值加上增量 increment。
如果字段不存在: 则先将字段的值初始化为 0,然后再执行加法操作。
如果字段的值不是整数类型: 会报错。
increment 可以是负数: 表示减法操作。
代码示例:
# 增加哈希字段的数值 r.hset("product:1001", "stock", 100) # 初始化库存 current_stock = r.hget("product:1001", "stock") print(f"初始库存: {current_stock.decode('utf-8')}") increment_result = r.hincrby("product:1001", "stock", 10) # 增加 10 个库存 print(f"增加库存后的值: {increment_result}") decrement_result = r.hincrby("product:1001", "stock", -5) # 减少 5 个库存 print(f"减少库存后的值: {decrement_result}") # 尝试对非数值字段进行 HINCRBY 操作 (会报错) # r.hincrby("user:1001", "name", 1) # 报错: redis.exceptions.ResponseError: ERR hash value is not an integer
详解:
r.hset("product:1001", "stock", 100): 初始化商品 "product:1001" 的库存字段 "stock" 为 100。
r.hincrby("product:1001", "stock", 10): 将 "stock" 字段的值增加 10,返回增加后的值。
r.hincrby("product:1001", "stock", -5): 将 "stock" 字段的值减少 5,返回减少后的值。
错误示例: 尝试对字符串类型的 "name" 字段执行 HINCRBY 操作,会抛出 redis.exceptions.ResponseError 异常,提示哈希值不是整数。
2.4.2.12 HSCAN - 迭代哈希中的字段
HSCAN key cursor [MATCH pattern] [COUNT count] 命令用于迭代哈希 key 中的字段。 当哈希包含大量字段时,使用 HGETALL 可能会阻塞服务器,HSCAN 可以分批次、迭代地获取字段,避免阻塞。
cursor: 游标,用于标识迭代的起始位置。初始游标为 0。
MATCH pattern: 可选参数,用于匹配字段名。
COUNT count: 可选参数,用于指定每次迭代返回的字段数量 (hint)。
代码示例:
# 准备大量哈希字段 for i in range(1000): r.hset("large_hash", f"field_{i}", f"value_{i}") cursor = '0' # 初始游标 count = 0 while cursor != '0' or count == 0: # 第一次循环 count 为 0,需要执行一次 cursor, data = r.hscan("large_hash", cursor=cursor, count=100) # 每次迭代获取 100 个字段 count += len(data) print(f"当前游标: {cursor}, 获取到 {len(data)} 个字段") # 处理获取到的数据 (data 是一个字典) # for field_bytes, value_bytes in data.items(): # field = field_bytes.decode('utf-8') # value = value_bytes.decode('utf-8') # print(f"字段: {field}, 值: {value}") print(f"总共迭代获取到 {count} 个字段")
详解:
准备数据: 我们首先创建了一个名为 "large_hash" 的哈希,并添加了 1000 个字段。
迭代过程:
初始化 cursor 为 '0'。
使用 r.hscan("large_hash", cursor=cursor, count=100) 进行迭代,每次获取最多 100 个字段。
r.hscan 返回一个元组 (cursor, data),其中 cursor 是新的游标值,data 是本次迭代获取到的字段和值 (字典)。
判断游标是否为 '0',如果为 '0',则表示迭代结束。
在循环中处理获取到的数据。
计数: count 变量用于统计总共迭代获取到的字段数量。
2.4.3 哈希 (Hash) 的内部实现
Redis 哈希的内部实现会根据哈希对象包含的键值对数量以及字段和值的长度,在 ziplist (压缩列表) 和 hashtable (哈希表) 两种数据结构之间进行动态切换。
ziplist (压缩列表): 当哈希对象满足以下两个条件时,Redis 会使用 ziplist 作为哈希的底层实现:
哈希对象保存的所有键值对的键和值的字符串长度都小于 hash-max-ziplist-value 配置项的值 (默认 64 字节)。
哈希对象包含的键值对数量小于 hash-max-ziplist-entries 配置项的值 (默认 512 个)。
ziplist 是一种为了节省内存而开发的特殊编码的线性数据结构,它将多个小的字符串或整数压缩存储在一块连续的内存区域中。 ziplist 的优点是节省内存,缺点是当哈希对象包含大量键值对或键值对的长度较大时,性能会下降。
hashtable (哈希表): 当哈希对象不满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的底层实现。
hashtable 就是我们通常所说的哈希表,它使用链地址法解决哈希冲突,具有高效的查找、插入和删除操作。 hashtable 的优点是性能稳定,缺点是相比 ziplist 会占用更多的内存。
数据结构转换:
当哈希对象最初使用 ziplist 编码,但随着键值对的增加或键值对长度的增加,不再满足 ziplist 的条件时,Redis 会自动将哈希对象的编码从 ziplist 转换为 hashtable。 这个转换过程是自动的,对用户透明。
可以通过 OBJECT ENCODING key 命令查看哈希对象的编码方式。
127.0.0.1:6379> OBJECT ENCODING user:1001 "hashtable" # 如果字段数量或字段值长度超过 ziplist 阈值,则编码为 hashtable 127.0.0.1:6379> OBJECT ENCODING small_hash "ziplist" # 如果字段数量和字段值长度都满足 ziplist 阈值,则编码为 ziplist
2.4.4 哈希 (Hash) 的最佳实践与注意事项
合理设计哈希键名: 哈希的键名应该具有明确的语义,能够清晰地表达哈希对象所代表的实体或数据类型。例如,user:用户ID,product:商品ID,session:会话ID 等。
控制哈希的大小: 虽然 Redis 哈希可以存储大量的字段值对,但为了保证性能,建议控制单个哈希对象的大小。 如果一个哈希对象包含的字段过多,可能会导致内存占用过高,并影响读取和写入性能。
避免大哈希: 如果需要存储大量相关数据,可以考虑将数据分散到多个哈希对象中,或者使用其他数据结构 (例如 Sorted Set 或 Redis 集群)。
使用批量操作: 对于批量读取或写入哈希字段的操作,尽量使用 HMGET 和 HSET (mapping) 等批量命令,可以减少网络 round-trip 次数,提高效率。
注意内存占用: 哈希的字段和值都是字符串,会占用内存。需要根据实际应用场景合理评估哈希的内存占用,并进行必要的优化。
考虑使用 TTL (Time To Live): 如果哈希对象的数据具有时效性,可以考虑设置过期时间 (TTL),让 Redis 自动删除过期的哈希对象,释放内存。
总结
Redis 哈希是一种非常强大且灵活的数据类型,它非常适合存储和操作结构化数据。通过本文的详细讲解和代码实践,相信你已经对 Redis 哈希有了更深入的理解。掌握哈希的用法,能够让你在实际项目中更加高效地利用 Redis 解决各种数据存储和访问问题。