十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Redis 初阶】Hash 类型深度解析:结构化数据存储的最优解

【Redis 初阶】Hash 类型深度解析:结构化数据存储的最优解 草莓熊Lotso个人主页❄️个人专栏:《C知识分享》 《Linux 入门到实践零基础也能懂》✨生活是默默的坚持毅力是永久的享受 博主简介文章目录前言一. 哈希类型基本介绍二. Hash 核心命令全解2.1 基础读写删hset /hget/hexists /hdel2.2 全量遍历hkeys /hvals2.3 批量操作hgetall /hmget2.4 辅助命令hlen /hsetnx/hincrby /hincrbyfloat2.5 命令小结三. Hash 底层编码实现3.1 两种编码方式3.2 编码切换条件3.3 设计思想时空权衡的经典体现四. Hash 典型应用场景对象缓存4.1 三种对象缓存方案对比4.2 Hash 与关系型数据库的区别4.3 工程实践细节结尾前言上一篇我们把 String 类型从命令到底层编码再到业务场景彻底讲透了很多业务用 string JSON 序列化就能完成缓存。但如果你的数据是结构化对象比如用户信息、商品详情经常需要读写单个字段每次都全量读取、反序列化、修改、再写回既笨重又浪费性能。这时候 Hash 类型就是更贴合场景的选择。很多初学者对 Hash 的认知停留在 “值里还能嵌套键值对”但对命令的生产风险、底层编码的切换逻辑、和其他存储方案的优劣对比一知半解。本文从基础概念讲起逐个拆解 Hash 核心命令的用法与踩坑点深入 ziplist 和 hashtable 两种底层编码的设计思想再结合对象缓存场景做完整的方案对比帮你把 Hash 类型从 “会用” 吃透到 “用好”。一. 哈希类型基本介绍哈希表是开发中最常用的数据结构之一不同语言里叫法不同C 里是unordered_mapJava 里是 HashMap本质都是键值对映射。Redis 本身就是一个大的键值对存储而 Hash 类型的特殊之处在于它的 value 本身又是一个键值对结构。为了和外层的 key-value 区分内层的键我们一般叫field对应的值叫value整体是key - { field1: value1, field2: value2 }的结构。举个最直观的例子存储一个用户的信息。 如果用 string 类型需要拆成多个独立的 keyuser:1:name - James user:1:age - 28 user:1:city - Beijing如果用 hash 类型一个 key 就能把所有属性装进去user:1 - { name - James age - 28 city - Beijing }很明显Hash 类型更适合存储结构化的对象数据相关的属性聚合在同一个 key 里内聚性更好。二. Hash 核心命令全解2.1 基础读写删hset /hget/hexists /hdel这四个是 Hash 最基础的增删改查命令时间复杂度都是 O (1)。hset设置字段值HSET key field value[field value...]支持一次设置一个或多个 field-value 对返回值是本次成功新增的字段个数。如果字段已经存在会覆盖旧值但不计入返回的新增计数。# 单个字段设置127.0.0.1:6379hset user:1 name James(integer)1# 批量设置多个字段127.0.0.1:6379hset user:1 age28city Beijing(integer)2hget读取字段值HGET key field根据 key 和 field 获取对应的值。如果 key 或 field 不存在返回 nil如果 key 的类型不是 hash会报错。127.0.0.1:6379hget user:1 nameJames127.0.0.1:6379hget user:1 gender(nil)hexists判断字段是否存在HEXISTS key field判断指定 field 是否存在于 hash 中存在返回 1不存在返回 0。127.0.0.1:6379hexists user:1 name(integer)1127.0.0.1:6379hexists user:1 gender(integer)0hdel删除指定字段HDEL key field[field...]删除 hash 中一个或多个字段返回值是成功删除的字段个数。 注意区分del删除的是整个 keyhdel删除的是 key 下的 field不要搞混。127.0.0.1:6379hdel user:1 age city(integer)22.2 全量遍历hkeys /hvals这两个命令用来获取 hash 中所有的 field 或者所有的 value时间复杂度为 O (N)N 是当前 hash 中 field 的数量。# 获取所有字段名127.0.0.1:6379hkeys user:11)name# 获取所有字段值127.0.0.1:6379hvals user:11)James这里必须强调一个生产风险和keys *类似如果某个 hash 里的 field 数量非常多hkeys、hvals会长时间遍历阻塞 Redis 主线程。不要因为它只操作一个 key 就掉以轻心大 hash 的全量遍历同样是高危操作。2.3 批量操作hgetall /hmgethgetall获取全部字段与值HGETALL key返回 hash 中所有的 field 和 value输出格式是 field 和 value 交替排列。127.0.0.1:6379hgetall user:11)name2)James3)age4)28这个命令的风险比 hkeys 更高因为它要返回所有字段名和字段值数据量更大阻塞时间更长。生产环境的大 hash 绝对禁止直接使用 hgetall。hmget批量读取多个字段HMGET key field[field...]和 string 的 mget 类似一次获取多个 field 的值返回结果的顺序和输入的 field 顺序一一对应。通过批量操作减少网络 IO 次数是非常实用的优化手段。127.0.0.1:6379hmget user:1 name age city1)James2)283)(nil)很多人会问有没有 hmset其实是有的但现在的 Redis 版本里hset本身已经支持批量设置多个字段了hmset 基本被替代日常开发直接用 hset 即可。更安全的替代方案hscan针对大 hash 的遍历需求Redis 提供了hscan命令属于渐进式遍历。它不会一次遍历完所有字段而是每次调用只返回一小部分多次调用完成全量遍历。 这种 “化整为零” 的思路把一次长时间阻塞拆成了多次短时间操作不会卡住主线程是生产环境遍历大集合的标准做法。这个思想和哈希表的渐进式 rehash 异曲同工后面讲源码的时候会再提到。2.4 辅助命令hlen /hsetnx/hincrby /hincrbyfloathlen获取字段总数HLEN key返回 hash 中 field 的总个数时间复杂度 O (1)。 很多人会疑惑为什么不用遍历就能拿到总数原理很简单底层结构里专门有一个变量记录元素个数直接读取即可不需要遍历。这也是典型的 “用少量空间换时间” 的设计。hsetnx字段不存在才设置HSETNX key field value和 string 的 setnx 逻辑一致只有当 field 不存在时设置才会成功如果 field 已经存在直接失败。原子性操作适合做字段级别的幂等控制。hincrby /hincrbyfloat字段数值增减HINCRBY key field increment HINCRBYFLOAT key field increment和 string 的 incr 家族类似对指定字段的数值做增减操作整数用 hincrby浮点数用 hincrbyfloat。同样是原子操作单线程下不会有并发问题。 Redis 没有提供 hdecrby传负数即可实现减法。2.5 命令小结命令作用时间复杂度hset key field value [field …]设置一个或多个字段值O (k)k 为字段数hget key field获取指定字段值O(1)hexists key field判断字段是否存在O(1)hdel key field [field …]删除一个或多个字段O (k)k 为字段数hlen key获取字段总数O(1)hkeys key获取所有字段名O (N)N 为字段总数hvals key获取所有字段值O (N)N 为字段总数hgetall key获取所有字段与值O (N)N 为字段总数hmget key field [field …]批量获取多个字段值O (k)k 为字段数hsetnx key field value字段不存在时才设置O(1)hincrby key field n字段整数值增减O(1)hincrbyfloat key field n字段浮点数值增减O(1)hstrlen key field获取字段值的字节长度O(1)学习 Redis 命令不用追求死记硬背用的时候多查官方文档、多动手敲自然就熟了。核心是理解每个命令的时间复杂度和生产风险。三. Hash 底层编码实现和 string 类型一样Hash 对外接口一致但底层会根据数据量自动选择编码方式在空间和时间之间做权衡。Hash 一共有两种底层编码ziplist压缩列表和hashtable哈希表。3.1 两种编码方式ziplist压缩列表当 hash 中元素数量少、每个值的长度都很短时Redis 会使用 ziplist 作为底层实现。 ziplist 是一块连续的内存所有元素紧挨着排列没有指针、没有空槽位空间利用率非常高。和普通哈希表相比它省去了哈希数组的空间开销和指针的额外占用内存占用小很多。代价也很明显读写元素需要遍历插入删除需要移动后续的内存数据性能会随着元素增多而下降。但在元素数量少的前提下这点性能差异完全可以忽略换来的内存收益非常可观。hashtable哈希表当数据量超过阈值后ziplist 的读写效率不足以支撑就会自动转成 hashtable 编码也就是我们熟悉的标准哈希表实现。它通过哈希函数定位元素读写时间复杂度 O (1)保证操作性能。代价是需要额外的哈希数组和指针开销内存占用更高。3.2 编码切换条件触发 ziplist 转 hashtable 有两个阈值都可以通过配置文件修改字段数量阈值hash-max-ziplist-entries默认 512。当字段数超过 512 个时转成 hashtable。值长度阈值hash-max-ziplist-value默认 64 字节。当任意一个字段的值长度超过 64 字节时转成 hashtable。两个条件满足任意一个就会触发编码转换。我们可以通过OBJECT encoding命令查看实际编码# 小数据量默认ziplist127.0.0.1:6379hset user:1 name James age28(integer)2127.0.0.1:6379OBJECT encoding user:1ziplist# 插入一个很长的值触发转换127.0.0.1:6379hset user:1 desc非常长的描述内容...(integer)1127.0.0.1:6379OBJECT encoding user:1hashtable3.3 设计思想时空权衡的经典体现很多人会问什么是 “压缩”通用压缩算法是 zip、gzip 那类而 ziplist 的 “压缩” 本质是针对数据结构的紧凑编码—— 去掉所有不必要的空间开销让数据尽可能紧密排列。普通哈希表为了 O (1) 的查询性能需要维持一个有空闲槽位的数组还要存指针本身就有空间浪费而 ziplist 完全放弃了哈希索引用连续内存紧凑存储用少量的性能损失换取了大幅的空间节省。这就是 Redis 编码优化的核心逻辑小数据量优先省内存大数据量优先保性能。Redis 里几乎所有数据类型的编码切换都遵循这个思路。源码视角从 ziplist 到 dict 的工程智慧站在 C/C 开发的视角看这两种编码的设计非常有代表性背后是两种经典的内存管理思路。ziplist极致的空间优化ziplist 的本质是一个连续分配的字节数组头部记录了总长度、尾部偏移、元素个数后面紧跟着一个个数据项。每个数据项里记录了前一项的长度、当前项的编码和内容。优点没有任何内存碎片和指针开销内存利用率拉满对 CPU 缓存非常友好。缺点插入、删除元素都需要做内存拷贝元素越多开销越大。所以它只适合小数据量场景这也是为什么阈值设为 512 个元素 —— 在这个量级下内存拷贝的代价完全可控而空间收益最大。dict哈希表与渐进式 rehashHash 类型的 hashtable 编码和 Redis 全局 key 的存储结构一样都是用 dict字典实现的也就是标准的数组 链表哈希表链地址法解决哈希冲突。哈希表有个经典问题扩容时需要把所有元素重新哈希到新数组里如果数据量很大这个过程会非常耗时导致服务卡顿。Redis 的解决方案是渐进式 rehash同时保留新旧两个哈希数组不一次性搬完所有数据而是每次操作哈希表的时候顺便搬几个元素慢慢把旧数组的数据全部迁移到新数组最后释放旧数组。这和 hscan 的 “化整为零” 思想完全一致把一次大的耗时操作拆分成很多次小操作分散到平时的请求里避免长时间阻塞主线程。Java 的 ConcurrentHashMap 扩容也用了类似的思路是高并发系统里非常经典的优化手段。四. Hash 典型应用场景对象缓存Hash 最核心的应用场景就是存储结构化的对象数据最典型的就是用户信息、商品信息这类和数据库表行对应的实体。4.1 三种对象缓存方案对比同样是缓存用户信息有三种常见实现方式我们来完整对比一下优缺点。方案一每个属性一个 string keysetuser:1:name Jamessetuser:1:age28setuser:1:city Beijing优点实现简单单属性修改灵活。缺点key 数量太多内存占用大同一个对象的属性分散在各处内聚性极差不好维护。结论基本没有实用价值。这里提一下 “高内聚、低耦合”高内聚就是把相关的东西放在一起好找好管理低耦合就是模块之间关联要弱改一处不会牵连另一处。第一种方案把同一个用户的属性拆得七零八落就是典型的低内聚代码写起来乱出问题也不好排查。方案二string JSON 序列化setuser:1{name:James,age:28,city:Beijing}优点结构清晰一个 key 搞定适合整体读写的场景。缺点如果只修改单个字段需要把整个 JSON 读出来、反序列化、修改、再序列化写回去开销大性能差。方案三hash 类型存储hset user:1 name James age28city Beijing优点结构直观和数据库表一一对应可以单独读写任意一个字段灵活高效数据内聚性好便于管理。缺点内存占用比 JSON 字符串略高需要注意编码切换的阈值避免大对象带来的问题。综合来看对于需要频繁操作单个属性的结构化对象Hash 类型是最合适的选择。4.2 Hash 与关系型数据库的区别很多人觉得 Hash 像数据库里的一行记录但两者有本质区别稀疏性不同Hash 是稀疏的每个 key 可以有完全不同的 field不需要的字段根本不存不占空间。 关系型数据库是结构化的一旦加了新列所有行都要有这个字段哪怕值是 null。 这也让 Hash 非常适合属性不固定的对象存储。查询能力不同关系型数据库支持复杂的条件查询、联表查询、聚合统计。 Redis Hash 只能按 key 和 field 做单点查询做复杂统计非常困难开发成本极高。所以两者是互补关系数据库存全量数据、做复杂查询Redis 做缓存、扛高频单点读写。4.3 工程实践细节有个很有意思的细节用户 id 已经在 key 里了比如user:1那 hash 里还要不要存 uid 字段 从节省空间的角度确实没必要。但在实际工程中大多数团队都会选择冗余存一份。原因很简单代码里拿到 hash 的数据后可以直接转成对象使用不用再从 key 里解析 id开发更方便代码也更简洁。这点空间开销换开发效率通常是划算的。核心考点总结最后梳理一下 Hash 类型的核心考点基本覆盖面试和工作的高频问题数据结构value 本身是 field-value 键值对适合存储结构化对象。核心命令hset/hget/hdel/hexists/hlen 的用法与时间复杂度hgetall、hkeys 的生产风险与替代方案 hscan。底层编码ziplist 与 hashtable 两种编码各自的优缺点与切换条件。设计思想小数据量省空间、大数据量保性能的时空权衡渐进式操作化整为零避免阻塞。方案对比三种对象缓存方案的优缺点Hash 相比 stringJSON 的优势。与数据库区别稀疏性与结构化的差异各自的适用边界。源码原理ziplist 连续内存的设计、dict 渐进式 rehash 的思路。结尾 我是草莓熊 Lotso若这篇技术干货帮你打通了学习中的卡点 【关注】跟我一起深耕技术领域从基础到进阶见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好需要时直接查、随时用 【评论】分享你的经验或疑问比如曾踩过的技术坑一起交流避坑 ️ 【投票】用你的选择助力社区内容方向告诉大家哪个技术点最该重点拆解 技术之路难免有困惑但同行的人会让前进更有方向愿我们都能在自己专注的领域里一步步靠近心中的技术目标结语Hash 是 Redis 里非常实用的一种数据结构尤其适合对象类的缓存场景比 stringJSON 更灵活比多 string 方案更内聚。理解它的底层编码和使用边界能帮你在业务里做出更合理的技术选型。下一篇我们会继续深入 List 类型看看它的命令、底层实现与典型业务场景。✨把这些内容吃透超牛的放松下吧✨ʕ˘ᴥ˘ʔづきらど
返回列表