导读:本期聚焦于澳门程序员创作的《Redis哈希Hash类型如何存储对象?常用命令与实战场景详解》,敬请观看详情。哈希类型是Redis五种基础数据结构之一,它特别适合用来存储对象。本文围绕Redis Hash存储对象这一核心话题,从底层存储原理讲起,分析hash-max-ziplist-entries配置对内存的影响,解释小哈希使用listpack压缩存储、大哈希转为hashtable的转换机制。接着详细介绍HSET、HGET、HMGET、HGETALL、HINCRBY等常用命令的用法和代码示例,并对比Hash与String存储对象的三种方案在内存占用和读写性能上的差异。最后结合购物车、用户资料缓存等实际场景,给出字段设计的注意事项和坑点总结,帮助你选对存储结构,写出更高效的Redis应用代码。

在Redis的实际应用中,存储对象是最常见的需求之一。比如用户信息、商品详情、配置项等等,本质上都是一个对象的多个字段。Redis提供了多种方式来存储这类数据,其中哈希类型是最贴合对象存储场景的结构。一个Hash可以看作一个Java里的Map,或者PHP里的关联数组,外层是一个key,内部是field-value的映射。这篇文章就来详细聊聊Hash类型的存储原理、常用命令,以及它和String存储对象方案的对比,帮你在实际项目中做出正确的选择。

Redis哈希Hash类型如何存储对象?常用命令与实战场景详解

一、Hash类型的基本概念与存储原理

Hash类型是一个键值对集合,其中每个键值对的键叫field,值叫value。它和Redis最外层的key-value不一样:最外层的key是字符串,而Hash内部的field-value都是字符串类型,且同一个Hash内部不允许存在重复的field。用一个形象的比喻,Redis最外层是一个大Map,Hash类型相当于在大Map的某个key下面又嵌套了一个小Map。

从底层实现来看,Hash类型的存储结构在Redis 7.0之后主要有两种编码方式:listpack和hashtable。当一个Hash的field数量较少且所有field和value的长度都较短时,Redis使用listpack编码,这是一种紧凑的顺序存储结构,所有数据在内存中连续排列,节省了大量指针开销。当满足一定条件后,比如field数量超过hash-max-listpack-entries配置的值(默认128),或者任意一个field或value的长度超过hash-max-listpack-value配置的值(默认64),Redis会把这个Hash从listpack转换为hashtable编码。

转换为hashtable之后,查找某个field的时间复杂度是O(1),但内存开销会明显上升,因为每个entry都需要额外的指针维护。这种设计思路是典型的空间换时间的权衡:小对象用紧凑结构省内存,大对象用哈希表保性能。可以通过下面的命令查看一个Hash的编码方式:

127.0.0.1:6379> HSET user:1 name zhangsan age 25
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"
127.0.0.1:6379> CONFIG GET hash-max-listpack-entries
1) "hash-max-listpack-entries"
2) "128"

二、Hash类型的常用命令详解

掌握Hash的命令是使用它的基础。最核心的一组命令是HSET和HGET,分别用于设置和获取单个field的值。HSET支持一次设置多个field-value对,这是后来版本合并了HMSET功能的结果,官方已经不推荐使用HMSET。还有一个非常实用的HSETNX命令,只有当field不存在时才设置值,适合做初始化操作。

批量读取方面,HMGET可以一次获取多个field的值,HGETALL则返回整个Hash的所有field和value。需要注意的是,如果Hash里的字段非常多,HGETALL是一个危险的命令,它会一次性返回全部数据,可能造成Redis阻塞。生产环境中对于大Hash,建议使用HSCAN渐进式遍历,避免一次性拉取带来的性能问题。

计数相关的命令有HINCRBY和HINCRBYFLOAT,它们可以对某个field的值做原子自增,前提是该值必须能被解析为整数或浮点数。这个特性在做计数器、库存扣减、积分累计时特别有用。下面用一段代码演示这些常用命令:

# 设置多个字段
HSET user:1001 name "lisi" age 28 city "beijing"
# 获取单个字段
HGET user:1001 name          # 返回 "lisi"
# 批量获取
HMGET user:1001 name age city
# 获取所有字段
HGETALL user:1001
# 删除指定字段
HDEL user:1001 city
# 字段原子自增
HINCRBY user:1001 age 1      # age 变为 29
# 判断字段是否存在
HEXISTS user:1001 age        # 返回 1
# 只获取所有字段名或字段值
HKEYS user:1001
HVALS user:1001
# 获取字段数量
HLEN user:1001

另外还有HSTRLEN命令可以获取指定field值的字符串长度,HINCRBYFLOAT用于浮点数自增。这些命令组合起来基本能覆盖对象读写的所有场景。

三、Hash存储对象与String存储对象的方案对比

用Redis存储对象其实有三种常见方案。第一种是原生String,直接把对象序列化成JSON字符串后存储;第二种是拆分成多个String的key,每个字段一个key;第三种就是使用Hash。三种方案各有优劣,需要根据读写模式来选择。

先说JSON序列化方案,它的优点是读写整个对象很方便,一次GET就能拿到全部数据,但缺点也很明显:只要修改一个字段,就必须把整个对象读出来、反序列化、修改后再序列化写回,在并发场景下还容易出现互相覆盖的问题。拆分成多个String key的方案虽然可以单独修改某个字段,但会产生大量碎片化的key,占用更多内存,而且无法集中管理,删除一个对象需要删除多个key,事务和过期控制都很麻烦。

Hash方案则取了两者的长处:既能像JSON方案一样集中在一个key下管理整个对象,又能通过field单独读写某个字段,修改年龄不需要动到名字,天然支持字段级别的原子操作。内存方面,处于listpack编码下的小Hash非常省空间。我们用表格对比一下三种方案:

对比项JSON存String拆分多个StringHash类型
整体读写方便,一次GET需多次GET或MGETHGETALL
单独修改字段需读出整个对象改后写回支持,单独SET支持,HSET即可
内存占用较低较高,key碎片多小Hash时很低
字段级原子计数不支持支持INCR支持HINCRBY
过期控制整个对象统一过期各key独立过期只能整个key过期

需要特别指出Hash的一个限制:过期时间只能设置在key级别,不能针对单个field设置过期。如果你的业务需要每个字段有独立的TTL,那Hash就无能为力了,可以考虑拆分key或者等新版本的相关特性。另外,Hash也不能像RedisJSON那样支持嵌套结构,field的value只能是字符串,复杂对象仍然需要序列化后存入field。

四、实战场景:用Hash实现购物车

购物车是Hash的经典应用场景。一辆购物车对应一个key,比如cart:用户ID,购物车里的每个商品对应一个field,商品数量作为value。这样添加商品、修改数量、删除商品、查询整个购物车都天然对应Hash的操作命令,逻辑非常清晰。下面是一段Java风格的示例代码:

// 添加商品到购物车,商品1001加入2件
jedis.hset("cart:1001", "goods:5001", "2");

// 商品数量加1,原子操作,无需担心并发问题
jedis.hincrBy("cart:1001", "goods:5001", 1);

// 获取某个商品的数量
String num = jedis.hget("cart:1001", "goods:5001");

// 移除商品
jedis.hdel("cart:1001", "goods:5001");

// 获取购物车中商品总数
long count = jedis.hlen("cart:1001");

// 获取整个购物车
Map<String, String> cart = jedis.hgetAll("cart:1001");

除了购物车,用户资料缓存也是Hash的典型用法。把用户的昵称、头像、等级等字段存进user:ID这个Hash里,页面展示时用HMGET只取需要的几个字段,避免了JSON方案下必须反序列化整个对象的开销。字段有更新时直接HSET对应field即可,缓存和数据库的同步维护也简单得多。

五、使用Hash的注意事项与坑点

第一个坑是bigkey问题。如果一个Hash的field数量达到几十万甚至上百万,HGETALL、HKEYS这类全量命令会严重阻塞Redis主线程。规范的做法是控制单个Hash的field数量,把大对象按一定规则拆分成多个小Hash,比如按用户ID取模分桶存储,每个桶控制在几千个field以内。

第二个坑是遍历时的错误做法。很多新手习惯用HKEYS去遍历大Hash,这在生产环境是明令禁止的。正确的做法是使用HSCAN配合游标渐进式获取,每次只拉取一小部分数据。需要注意的是,HSCAN保证的是遍历期间完整返回所有存在的元素,但如果遍历过程中有数据被修改,可能会返回重复或遗漏,业务侧需要做好幂等处理。

第三个坑是编码转换的性能抖动。当Hash从listpack转换为hashtable时会发生一次性的内存重分配,如果业务上Hash的字段数量刚好在阈值附近来回波动,可能造成频繁转换。这种情况可以通过调大hash-max-listpack-entries来缓解,但要权衡内存增长,或者直接在业务上控制字段数量的稳定性。理解了这些原理和边界,Hash类型就能在你的系统里发挥出最大的价值。

Redis Hash哈希类型存储对象修改时间:2026-09-07 18:54:56

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/52380.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。