5.Hash 【由浅入深-redis】

文章目录

  • [第一章:Redis 哈希(Hash)核心机制与命令全解](#第一章:Redis 哈希(Hash)核心机制与命令全解)
    • [1.1 基础单字段读写与判定](#1.1 基础单字段读写与判定)
      • [HSET 命令](#HSET 命令)
      • [HGET 命令](#HGET 命令)
      • [HEXISTS 命令](#HEXISTS 命令)
      • [HDEL 命令](#HDEL 命令)
    • [1.2 批量操作与全量获取的高危陷阱](#1.2 批量操作与全量获取的高危陷阱)
      • [HMGET 与 HMSET 命令](#HMGET 与 HMSET 命令)
      • [HKEYS、HVALS 与 HGETALL 命令](#HKEYS、HVALS 与 HGETALL 命令)
    • [1.3 渐进式遍历 HSCAN 与扩容机制](#1.3 渐进式遍历 HSCAN 与扩容机制)
      • [HSCAN 命令](#HSCAN 命令)
    • [1.4 长度统计、存在性判定与数值计算](#1.4 长度统计、存在性判定与数值计算)
      • [HLEN 命令](#HLEN 命令)
      • [HSETNX 命令](#HSETNX 命令)
      • [HINCRBY 与 HINCRBYFLOAT 命令](#HINCRBY 与 HINCRBYFLOAT 命令)
      • [HSTRLEN 命令](#HSTRLEN 命令)
  • [第二章:Redis 哈希的底层编码与内存优化](#第二章:Redis 哈希的底层编码与内存优化)
    • [2.1 压缩的本质:重新编码与内存节约](#2.1 压缩的本质:重新编码与内存节约)
    • [2.2 内存与性能的权衡:ziplist 与 hashtable 之争](#2.2 内存与性能的权衡:ziplist 与 hashtable 之争)
    • [2.3 触发编码转换的两个核心条件](#2.3 触发编码转换的两个核心条件)
    • [2.4 关键配置参数:redis.conf 中的调控开关](#2.4 关键配置参数:redis.conf 中的调控开关)
  • 第三章:哈希存储设计与工程实践原则
    • [3.1 结构化数据存储:字符串(JSON)与哈希的选型对比](#3.1 结构化数据存储:字符串(JSON)与哈希的选型对比)
    • [3.2 数据模型演进:原生字符串的"低内聚"困境](#3.2 数据模型演进:原生字符串的“低内聚”困境)
    • [3.3 软件工程心法:高内聚与低耦合的哲学](#3.3 软件工程心法:高内聚与低耦合的哲学)
    • [3.4 哈希存储的最佳实践:关于 uid 字段的思考](#3.4 哈希存储的最佳实践:关于 uid 字段的思考)

第一章:Redis 哈希(Hash)核心机制与命令全解

Redis 自身在底层就是一个键值对(key-value)结构,而它自身的键值对正是通过哈希的方式来组织的。

在日常开发与技术面试中,哈希(Hash)结构属于高频考察点,也是使用频率极高的数据结构之一。

在 Redis 中,哈希类型是指 value 本身又是一个键值对结构 ,形如 key = "key",value = { { field1, value1 }, ..., { fieldN, valueN } }。也就是说,把 key 这一层组织完毕后,到了 value 这一层,value 的具体类型还可以再是一个哈希。

这里需要明确一个术语区分:哈希类型中的映射关系通常称为 field-value(字段-值),用于区分 Redis 整体的键值对(key-value)。这里的 value 是指 field 对应的值,不是 key 对应的值,在不同上下文中请留意 value 的作用范围。

1.1 基础单字段读写与判定

HSET 命令

  • 语法 :HSET key field value [field value ...]

  • 功能:设置 hash 中指定的字段(field)的值(value)。

  • 时间复杂度 :插入一组 field 为 O(1),插入 N 组 field 为 O(N)

  • 返回值 :添加的字段的个数。

  • 示例 :

    redis 复制代码
    redis> HSET myhash field1 "Hello"
    (integer) 1
    redis> HGET myhash field1
    "Hello"

HGET 命令

  • 语法 :HGET key field

  • 功能:获取 hash 中指定字段的值。

  • 时间复杂度 :O(1)

  • 返回值 :字段对应的值或者 nil。

  • 示例 :

    redis 复制代码
    redis> HSET myhash field1 "foo"
    (integer) 1
    redis> HGET myhash field1
    "foo"
    redis> HGET myhash field2
    (nil)

HEXISTS 命令

  • 语法 :HEXISTS key field
  • 功能:判断 hash 中是否有指定的字段。
  • 时间复杂度 :O(1)
  • 返回值 :1 表示存在,0 表示不存在。
  • ⚠️ 风险警示 :这个操作存在一定的风险,类似于字符串章节提到的 KEYS * 命令。主要问题在于我们无法预知这个 hash 中是否会存在大量的 field。如果存在大量的 field,这个操作可能会带来不可预知的性能阻塞,在性能敏感的场景下务必谨慎使用。

HDEL 命令

  • 语法 :HDEL key field [field ...]

  • 功能 :删除 hash 中指定的字段。需要严格区分的是,DEL 删除的是整个 key,而 HDEL 删除的是哈希内部的 field。

  • 时间复杂度 :删除一个元素为 O(1),删除 N 个元素为 O(N)。

  • 返回值 :本次操作删除的字段个数。

  • 示例 :

    redis 复制代码
    redis> HSET myhash field1 "foo"
    (integer) 1
    redis> HDEL myhash field1
    (integer) 1
    redis> HDEL myhash field2
    (integer) 0

1.2 批量操作与全量获取的高危陷阱

HMGET 与 HMSET 命令

  • HMGET 语法 :HMGET key field [field ...]
  • 功能 :一次获取 hash 中多个字段的值。类似于字符串类型的 MGET,HMGET 可以一次查询多个 field,而 HGET 一次只能查询一个 field 。注意 :返回的多个 value 的顺序和请求的 field 的顺序是完全匹配的。
  • HMSET 语法 :HMSET key field value [field value ...]
  • 功能 :早期用于一次设置多个 field 和 value。现在已不需要使用,因为 HSET 已经完全支持一次设置多个 field 和 value。
  • 时间复杂度 :只查询一个元素为 O(1),查询多个元素为 O(N)(N 为查询元素个数)。
  • 返回值 :字段对应的值或者 nil。
bash 复制代码
# 使用 HMSET 批量设置字段(早期用法)
redis> HMSET user:1 name James age 28 city Beijing
OK

# 使用 HMGET 批量获取字段,顺序与请求一致
redis> HMGET user:1 name age city nofield
1) "James"
2) "28"
3) "Beijing"
4) (nil)

HKEYS、HVALS 与 HGETALL 命令

  • HKEYS 语法 :HKEYS key,获取 hash 中的所有字段。返回字段列表。
  • HVALS 语法 :HVALS key,获取 hash 中的所有的值。返回所有的值。
  • HGETALL 语法 :HGETALL key,获取 hash 中的所有字段以及对应的值。返回字段和对应的值。
  • 时间复杂度 :O(N),N 为 field 的个数。
  • ⚠️ 性能风险 :H 系列的命令必须保证 key 对应的 value 是哈希类型的,否则会报错。上述 HKEYS、HVALS、HGETALL 操作的时间复杂度都是 O(N)。如果哈希非常大,这个操作就可能导致 Redis 服务器被阻塞。上述这样的操作,开销是比较大的。多数情况下,我们不需要查询所有的 field,可能只需要查询其中的几个 field。
bash 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1

# 获取所有字段名
redis> HKEYS myhash
1) "field1"
2) "field2"

# 获取所有值
redis> HVALS myhash
1) "Hello"
2) "World"

# 获取所有字段和值,按 field1, value1, field2, value2 顺序返回
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "World"

1.3 渐进式遍历 HSCAN 与扩容机制

HSCAN 命令

  • 语法 :HSCAN key cursor [MATCH pattern] [COUNT count]
    • 语法中 key 表示要扫描的 Hash 键名,cursor 表示游标位置 (第一次扫描传入 0,之后使用 Redis 返回的新游标继续扫描,直到返回 0 表示结束);MATCH pattern 是可选参数,用于按照匹配规则过滤字段名,例如 MATCH user:* 只返回符合规则的 field;COUNT count 也是可选参数,用于提示 Redis 每次扫描返回的大致数量,例如 COUNT 10 表示希望每次获取约 10 个字段 。完整流程是:第一次执行 HSCAN key 0 获取部分 field-value 和新的游标,随后不断使用新游标执行 HSCAN key cursor,直到游标重新变为 0,完成整个 Hash 的遍历。
  • 功能 :因为 HKEYS、HVALS、HGETALL 都存在一定的风险,如果 hash 的元素个数太多,执行耗时会比较长,从而阻塞 Redis。HSCAN 用于遍历 Redis 的 hash,它属于"渐进式遍历"。
  • 机制 :每次调用 HSCAN,只遍历 hash 的一小部分; 再调用一次,再遍历一小部分...... 连续执行多次,就可以完成整个遍历过程。这种化整为零的方式,使得时间就是可控的。
bash 复制代码
# 创建 Hash 数据
HSET user:1001 name Tom age 20 city Beijing email tom@test.com job engineer

# 第一次扫描,从 cursor=0 开始
HSCAN user:1001 0 COUNT 2

# 假设返回:
# 1) "4"                  <-游标,不计算数量
# 2) name Tom age 20      <-COUNT 只影响 Redis 希望返回多少个 field-value 数据,不影响 cursor。

# 使用返回的 cursor 继续扫描
HSCAN user:1001 4 COUNT 2

# 假设返回:
# 1) "8"
# 2) city Beijing email tom@test.com

# 继续扫描
HSCAN user:1001 8 COUNT 2

# 假设返回:
# 1) "0"                  <- cursor 为 0,遍历结束
# 2) job engineer


# 使用 MATCH 过滤字段
HSCAN user:1001 0 MATCH name* COUNT 10
# 只返回 field 名匹配 name* 的数据

1.4 长度统计、存在性判定与数值计算

HLEN 命令

  • 语法 :HLEN key
  • 功能:获取 hash 中的所有字段的个数。
  • 时间复杂度 :O(1)。不需要遍历,直接获取。
  • 返回值 :字段个数。
bash 复制代码
redis> HSET user:1 name James age 28
(integer) 2
redis> HLEN user:1
(integer) 2

HSETNX 命令

  • 语法 :HSETNX key field value
  • 功能 :在字段不存在的情况下,设置 hash 中的字段和值。类似于 SETNX,存在则设置失败。
  • 时间复杂度 :O(1)
  • 返回值 :1 表示设置成功,0 表示失败。
bash 复制代码
# 字段不存在,设置成功
redis> HSETNX user:1 email "james@example.com"
(integer) 1

# 字段已存在,设置失败
redis> HSETNX user:1 name "Tom"
(integer) 0

# 查看结果,name 仍然是原值
redis> HGET user:1 name
"James"

HINCRBY 与 HINCRBYFLOAT 命令

  • HINCRBY 语法 :HINCRBY key field increment
  • HINCRBYFLOAT 语法 :HINCRBYFLOAT key field increment
  • 功能 :hash 这里的 value,也可以当做数字来处理。HINCRBY 可以加/减整数,HINCRBYFLOAT 可以加/减小数。
  • 时间复杂度 :O(1)
  • 返回值 :该字段变化之后的值。
  • 注意事项 :使用频率不算很高。Redis 没有提供类似于 HINCR、HDECR 这样专门用于自增自减的命令,而是通过传入负数来实现减法操作。
bash 复制代码
# HINCRBY 整数加减
redis> HSET stats count 10
(integer) 1
redis> HINCRBY stats count 5
(integer) 15
redis> HINCRBY stats count -3
(integer) 12

# HINCRBYFLOAT 浮点数加减
redis> HSET stats price 10.50
(integer) 1
redis> HINCRBYFLOAT stats price 0.1
"10.6"
redis> HINCRBYFLOAT stats price -5
"5.6"

HSTRLEN 命令

  • 语法 :HSTRLEN key field
  • 功能:计算 value 的字符串长度。
  • 时间复杂度 :O(1)
  • 返回值 :字符串长度。
bash 复制代码
redis> HSET user:1 name James
(integer) 1
redis> HSTRLEN user:1 name
(integer) 5

第二章:Redis 哈希的底层编码与内存优化

在业务中使用哈希时,往往容易忽略 Redis 底层为了平衡内存与性能所做出的精妙设计。本章深入底层,剖析哈希的两种核心编码方式:ziplist 与 hashtable,揭示其空间与时间的博弈。

2.1 压缩的本质:重新编码与内存节约

压缩的本质,是对数据针对性的重新编码。例如,将包含大量重复字符的字符串 abcd000...000efgh 重新编码为 abcd0[100]efgh,即可大幅缩短长度。常见的 rar、zip、gzip 等算法皆是如此。

ziplist(压缩列表)也是同理。Redis 内部的数据结构经过精心设计,其核心目的就是节省内存空间。

一个普通的哈希表首先是一个数组,数组中必然存在空槽位,这会带来内存浪费;而 ziplist 通过紧凑的连续内存布局 如:[field1, value1, field2, value2, ..., fieldN, valueN],极大减少了这种浪费。

2.2 内存与性能的权衡:ziplist 与 hashtable 之争

ziplist 节省内存的代价是:读写元素的速度较慢 。因为需要遍历或重新分配连续内存 。元素少时这种慢并不明显;元素太多时,慢就会雪上加霜,导致 Redis 耗费大量 CPU 时间甚至阻塞。

因此,Redis 必须在内存和性能之间寻找平衡点。当数据量小、值短时,优先使用 ziplist节省内存;当数据量大、值长时,必须转换为hashtable 保证读写性能。

2.3 触发编码转换的两个核心条件

Redis 会在满足以下任一条件时,自动将哈希编码从 ziplist 转换为 hashtable:

  1. 元素个数维度:哈希中的元素个数超过阈值。
  2. 值长度维度:哈希中任何一个 value 的长度超过阈值。

只要触发其中一条,Redis 就会果断放弃 ziplist,转而使用 hashtable,防止过高的时间开销拖垮整个服务。

2.4 关键配置参数:redis.conf 中的调控开关

上述临界点通过配置文件明确定义,可直接写入 redis.conf:

  • hash-max-ziplist-entries :允许的最大元素个数。默认配置为 512 个。超过则转换为 hashtable。
  • hash-max-ziplist-value :允许的最大单值长度。默认配置为 64 字节 。超过则转换为 hashtable。

第三章:哈希存储设计与工程实践原则

在实际的业务开发中,我们经常面临数据模型的选型。Redis 的字符串(String)虽然也能作为缓存使用,但如果是存储结构化的数据,使用哈希(Hash)类型会更加合适。这类似于数据库中表的结构,哈希能够将对象的不同属性聚合在一起,实现高效的读写。

3.1 结构化数据存储:字符串(JSON)与哈希的选型对比

假设我们需要存储一个用户对象(UserInfo),包含 uid、name、age、city 等字段。

如果使用 String(JSON 格式) 来表示 UserInfo,当我们只需要获取或者修改其中某个 field(比如只修改 age)时,整个流程会非常繁琐:需要把整个 JSON 字符串读取出来,解析成对象,修改字段,然后再重新序列化为 JSON 字符串写回去。这显然带来了极大的 CPU 和网络开销。

而如果使用 Hash 的方式来表示 UserInfo,我们就可以使用 field 表示对象的每个属性(类似数据表的每一列),此时就可以非常方便地修改/获取任意一个属性的值。这种读写方式更直观、更高效。

然而,天下没有免费的午餐。使用哈希确实读写更高效,但付出的代价是空间代价 。我们需要严格控制在 ziplist 和 hashtable 两种内部编码之间的转换,因为一旦转换不当,可能会造成内存的较大消耗。因此,数据的合理拆分与组织变得尤为关键。

3.2 数据模型演进:原生字符串的"低内聚"困境

面对对象属性存储,如果不使用哈希,还有一种常见的做法是使用原生字符串类型,将每个属性作为一个独立的 key:

text 复制代码
set user:1:name James
set user:1:age 28
set user:1:city Beijing

这种做法本质上相当于把同一个数据对象的各个属性,给完全分散开表示了。这就是典型的低内聚。

低内聚有什么坏处?我们可以用生活中的场景类比:

就好比你的衣服,如果随意乱扔,会出现很多地方------比如床上、床头柜上、沙发上、椅子上、地上、晾衣架上......唯独不会出现在衣柜里。这种"低内聚"的情况,导致想找某个衣服或者找一个小的物件时,就会变得非常困难,就需要把各个角落都遍历一遍。如果把这些衣服都收在衣柜里,找起来就会非常方便。

在软件工程中,我们追求的恰恰相反:把有关联的东西放在一起,放在最能发挥它们作用的地方(高内聚),这才是好的代码设计。

3.3 软件工程心法:高内聚与低耦合的哲学

高内聚往往伴随着低耦合。这里的"耦合",指的是两个模块/代码之间的关联关系。关联关系越大,越是相互影响,认为是耦合越大;反之则越小。

"高内聚、低耦合"是软件工程中追求的核心目标。这可以通过两个生动的案例来理解:

  1. 拔智齿(低耦合的案例) :以前有朋友生病了,去医院拔阻生智齿(通常需要进行复杂的切口、切骨头、缝合)。当时他推掉了所有的课,去医院住院,全程照顾。手术仅仅是把智齿拔掉,如果非要说影响,总不可能把整个脑子也切掉吧?这个拔牙手术,仅仅是局部的操作,不应该影响其他器官的正常运作。这就是低耦合------牵一发而不动全身。
  2. 前任(高耦合的案例) :交往了一年,分开后偶然遇到,突然找你聊天。这种重新建立联系的"模块调用",往往带有很强的目的性(比如借钱、结婚要份子钱、推销微商产品等)。这种一旦联系,必然会严重影响你当前的生活状态,这就是高耦合------牵一发而动全身。

我们在写代码时,最怕的就是"牵一发而动全身",这边改一个 Bug,结果影响了其他模块,导致了更严重的问题。因此,在 Redis 的数据结构设计上,Hash 是"高内聚"的绝佳体现,因为它将同一个对象的数据封装在了一个 key 内部,修改某个字段不会影响其他字段,也不会影响其他 key。

3.4 哈希存储的最佳实践:关于 uid 字段的思考

在使用 Hash 存储对象时,我们会面临一个工程细节的考量:既然 key 已经是 user:1 了,那么 Hash 内部的 field 还需要存一个 uid 吗?如上图

从理论上讲,uid 确实没有必要存,我们可以直接使用 key 中的 id 来进行区分,这样是不是能存储空间进一步节省了呢?

如果确实想不存这个 uid 也可以,但是在真实的工程实践中,一般还是会把 uid 在 value 中再存一份。虽然这会多消耗一点点存储空间,但这其实是"高内聚"的体现------它保证了 value 本身是一份完整的、自包含的数据。

从工程实用的角度来看,多存一份 uid,后续写相关的代码,用起来会比较方便。例如,当后台管理系统需要批量扫描所有的用户 key,并提取出用户信息进行展示或导出时,如果 value 中包含了 uid,代码就不需要去解析 key 的字符串来截取 uid,直接读取 field 即可,提升了代码的可读性和可维护性。这种冗余是值得的。

相关推荐
lisw051 小时前
算法与软件的本质区别
驱动开发·算法·数学建模
Nuanyt1 小时前
LeetCode Hot 100 · 双指针 3 题 Java
java·笔记·算法·leetcode·双指针
Java小生不才2 小时前
SPRING AI向量化
redis·spring ai
志起计算机编程2 小时前
Clickhouse的几个关键优势
算法
重生之小比特2 小时前
【算法拾光 7】双指针|两数之和 II - 输入有序数组 + 三数之和
算法
Navigator_Z2 小时前
LeetCode //C - 1304. Find N Unique Integers Sum up to Zero
c语言·算法·leetcode
Lucis__13 小时前
Redis持久化机制解析
redis·缓存·操作系统·文件·非关系型数据库·1024程序员节
企业数字化笔记14 小时前
视频人体姿态怎么做三维可视化?2D关键点、深度局限与技术选型
人工智能·算法·音视频