当你要存储的对象包含多个字段时,使用普通字符串会面临一个尴尬的选择:将整个对象序列化为 JSON 并在每次变更时重写整个对象,或者将各个字段分散存储在多个独立的键中。哈希是适合存储对象的结构。它在一个键下存储字段-值对,允许你读取和更新单个字段,并将相关数据分组,这正是你存储用户记录、会话或缓存行时所需要的。
这是 Redis 大师班的第 4 部分,紧接在 字符串和计数器 之后。
基本操作
哈希是一个映射,在单个键下将字段映射到值:
HSET user:1 name "Aman" plan "pro" logins "42"
HGET user:1 plan # "pro"
HGETALL user:1 # 所有字段和值
HMGET user:1 name plan # 同时获取多个字段
HDEL user:1 logins # 删除一个字段
Enter fullscreen mode Exit fullscreen mode
相比序列化的 JSON 字符串,优势在于可以直接访问字段。要读取一个字段,你可以使用 HGET,而不是获取并解析整个对象。要更新一个字段,你可以使用 HSET,而不是读-改-写整个数据块。在大型对象上,这既更快,又在并发环境下更安全,因为两个客户端更新不同字段不会相互覆盖。
为什么不直接将 JSON 存储在字符串中?
这是人们纠结的选择,所以值得明确说明。将对象存储为 JSON 字符串既简单,当你总是同时读取和写入整个对象时(如替换整个缓存条目)也很合适。当出现以下情况时,哈希更好:
- 你经常读取或更新单个字段。
HGET user:1 plan胜过获取并解析整个 JSON 块来读取一个字段。 - 多个客户端并发更新不同字段。对于 JSON,两个写入者各自读-改-写整个对象,其中一个会丢失另一个的更改。对于哈希,
HSET不同字段是独立的且安全的。 - 你需要递增一个数字字段。
HINCRBY以原子方式增加一个字段,这是 JSON 字符串无法做到的,因为它需要读-改-写竞争。
规则:对于整个对象的访问,JSON 字符串没问题;对于字段级访问或并发字段更新,使用哈希。
原子字段操作
与字符串类似,哈希具有原子数字操作,但作用于字段:
HINCRBY user:1 logins 1 # 原子方式对 logins 字段 +1
HINCRBY cart:99 item:42 2 # 对 item 42 的数量 +2
HINCRBYFLOAT account:1 balance 9.99
Enter fullscreen mode Exit fullscreen mode
这对于包含计数器的对象非常有用。将购物车存储为哈希(每个产品一个字段,值为数量),你可以原子地使用 HINCRBY 调整一个商品的数量,而其他字段保持不变。每个用户的统计对象可以独立且安全地增加 logins 或 posts。你同时获得了对象分组和每字段原子计数器。
内存效率
哈希在规模上有一个很好的特性:对于小型哈希,Redis 使用紧凑的内存编码(listpack),这比将每个字段存储为单独的顶级键更节省空间。因此,将对象的字段分组到一个哈希中比等效的独立键集合使用更少的内存,同时还能将它们逻辑上保持在一起。
这就是为什么存储许多小对象的常见建议是使用哈希而不是每个字段一个键。一百万个用户记录作为哈希比相同数据分散在数百万个独立字符串键中占用更少的内存,而且更容易管理(每个用户一个键,一个 DEL 即可完全删除一个用户)。
过期:唯一的注意事项
有一个限制需要了解。历史上,Redis 中的过期(TTL)适用于整个键,而不是单个哈希字段。你可以过期整个 user:1 哈希,但不能仅过期其 logins 字段。这意味着每字段过期需要变通方法或不同的结构。
最近的 Redis 版本(7.4 及更高版本)添加了使用 HEXPIRE 为单个哈希字段设置 TTL 的能力,消除了历史限制:
HEXPIRE user:1 3600 FIELDS 1 session_token # 1 小时后过期一个字段
Enter fullscreen mode Exit fullscreen mode
如果你使用的是旧版本,请记住 TTL 是键级别的:要过期每字段数据,你需要将这些字段存储在它们自己的键下,或者升级到支持 HEXPIRE 的版本。对于大多数对象存储用途,你反正会过期整个哈希(整个会话、整个缓存记录),所以这很少成为问题。
何时使用哈希
当你要存储一个字段可以独立访问或更新的对象时,使用哈希:
- 用户记录和配置文件:字段逐个读取和更新。
- 会话:具有多个属性的会话对象,作为一个单元过期。
- 缓存的数据库行:将行的列作为字段,允许单独更新。
- 包含嵌入计数器的对象:购物车、每个实体的统计信息,任何具有需要原子增加的数字字段的对象。
当你只读写整个对象,且单个序列化块的简单性胜过字段级访问时,请使用 JSON 字符串。
哈希是 Redis 在不放弃字段级访问或每字段原子更新的情况下存储结构化对象的方式。它们将相关数据分组在一个键下,为小对象节省内存,并让并发写入者安全地修改不同字段。当你的值是一个对象而非单个事物时,哈希几乎总是正确的结构。
接下来,我们将介绍列表:支持队列和最近项目提要的有序序列,以及将它们转变为简单工作队列的阻塞操作。
要点
- 哈希在一个键下存储字段-值对,提供对单个字段的直接访问,而不是整个序列化块。
- 当你单独访问字段或多个客户端并发更新不同字段时,优先使用哈希而非 JSON 字符串。
-
HINCRBY原子地增加单个字段,适合包含嵌入计数器的对象,如购物车和每个实体的统计信息。 - 小型哈希使用紧凑的内存编码,因此将字段分组比分开存储许多独立键更节省空间。
- TTL 历史上是键级别的;Redis 7.4+ 通过
HEXPIRE添加了每字段过期,但你通常反正会过期整个哈希。
常见问题
何时应该使用 Redis 哈希而不是 JSON 字符串?
当你经常读取或更新单个字段,或多个客户端并发更新不同字段时,使用哈希,因为哈希允许独立的字段访问和更新。当你总是同时读写整个对象时,使用 JSON 字符串。
可以在 Redis 哈希中增加单个字段吗?
可以,使用 HINCRBY(或 HINCRBYFLOAT),它以原子方式在不影响其他字段的情况下增加一个字段的数值。这对于包含嵌入计数器的对象(如购物车中每个商品的数量)非常理想。
哈希比单独的键更节省内存吗?
对于许多小对象,是的。Redis 对小型哈希进行紧凑编码,因此将一个对象的字段分组到一个哈希中比将每个字段存储为自己的顶级键使用更少的内存,并且数据在逻辑上保持分组。
可以为单个哈希字段设置过期时间吗?
历史上不可以,TTL 仅适用于整个键。Redis 7.4 及更高版本添加了 HEXPIRE 支持每字段 TTL。在旧版本上,你需要过期整个哈希或将具有每字段过期的数据存储在单独的键下。
如何在 Redis 中存储用户对象?
使用类似 user:1 的键作为哈希,为每个属性设置字段(name、plan、logins)。然后你可以直接读取或更新任何字段,原子地增加数字字段,并使用单个 DEL 删除整个用户。
延伸阅读
本文最初发布于 amanksingh.com/blog/redis-hashes。
关于作者
Aman Kumar Singh 是印度诺伊达的一名团队负责人兼高级软件工程师,撰写关于全栈工程、系统设计以及使用 TypeScript、Next.js、NestJS、PostgreSQL 和 Redis 构建生产级 SaaS 的文章。
- 作品集: amanksingh.com
- GitHub: amansingh1501
- LinkedIn: amansingh1597
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.