掌握 Redis GET 和 SET:基础数据操作

通过这份关于 `GET` 和 `SET` 命令的全面指南,掌握 Redis 数据管理的基础。学习基本的字符串存储与检索,并探索关键的高级选项,如原子性设置(`NX`/`XX`)和集成键过期(`EX`/`PX`)。了解这些基础命令如何对构建高性能缓存层至关重要。

掌握 Redis GET 和 SET:基础数据操作

Redis 的 GETSET 看起来几乎过于简单。写入一个值。读取一个值。在实际应用中,这两个命令支撑着登录会话、功能开关、速率限制、缓存条目、短期锁以及“请不要再查询数据库”的快捷方式。

细节很重要,因为 Redis 会精确执行你的指令。如果你意外覆盖了一个键,它不会询问你是否是有意的。如果你忘记为缓存数据设置过期时间,它可能比数据源存活更久。如果你将缺失的键与空字符串等同处理,你的应用程序可能会做出错误的决策。

Redis 键值模型

在深入命令之前,重要的是要记住 Redis 运行在简单的键值存储模型上。每条数据(值)都通过一个唯一标识符(键)来访问。键是字符串,值可以是多种数据类型(字符串、列表、集合、哈希等)。SETGET 主要处理字符串数据类型,这是 Redis 中最基本且最常用的类型。

1. 设置数据:SET 命令

SET 命令用于为键赋值。如果该键已持有数据,SET 命令将覆盖现有值。其基本语法很直接。

基本语法和用法

最简单的形式只需要键和值:

SET key value

示例:存储用户的显示名称:

127.0.0.1:6379> SET user:100:name "Alice Johnson"
OK

127.0.0.1:6379> GET user:100:name
"Alice Johnson"

高级 SET 选项:NX、XX 和过期时间

SET 的强大之处在于其可选参数,这些参数允许原子性的条件设置和生存时间(TTL)管理。这些选项对于正确实现锁和缓存至关重要。

条件设置:NXXX

这些选项控制何时执行设置操作,防止意外覆盖或确保仅当键存在时才进行覆盖。

  • NX(不存在): 仅当键存在时才设置。这对于“仅创建”操作和简单的锁模式很有用。

    SET my_lock_key some_unique_value NX
    
  • XX(存在): 仅当键已经存在时才设置。当你想要刷新一个已知键而不意外创建新键时,这很有用。

    SET session:token:456 new_value XX
    

B. 设置过期时间(TTL)

为了管理内存并实现基于时间的缓存,你可以直接在 SET 命令中设置过期时间。这比先设置键再单独调用 EXPIRE 高效得多。

  • EX seconds为单位设置过期时间。
  • PX milliseconds毫秒为单位设置过期时间。
  • EXAT timestamp 将过期时间设置为特定的 Unix 时间戳(秒)。
  • PXAT timestamp 将过期时间设置为特定的 Unix 时间戳(毫秒)。

示例:设置一个键在一小时后过期:

127.0.0.1:6379> SET cache:product:500 "Product Details" EX 3600
OK

127.0.0.1:6379> TTL cache:product:500 
(integer) 3598 

对于缓存条目,使用 SET key value EX NPX N。它将写入和过期操作合并为一个命令,避免了应用程序写入缓存键后在调用 EXPIRE 之前崩溃的常见错误。

组合选项

所有选项通常可以组合用于复杂的原子操作:

# 仅当键不存在时设置,并使其在 60 秒后过期
SET my_config_setting "active" NX EX 60

2. 检索数据:GET 命令

GET 命令检索与给定键关联的字符串值。它是 Redis 执行最快的操作之一,通常在微秒内完成。

基本语法和用法

GET key

示例: 检索存储的用户名

127.0.0.1:6379> GET user:100:name
"Alice Johnson"

处理不存在的键

如果键不存在,GET 返回一个特殊响应,指示未找到任何内容:

127.0.0.1:6379> GET non_existent_key
(nil)

在应用程序代码中,接收 (nil) 是确定数据缺失的标准方式,通常会触发缓存未命中,此时应用程序必须从主数据源(如数据库)获取数据,然后将其写回 Redis。

获取值的同时更改过期时间:GETEX

基本的 GET 命令只返回值。它不返回剩余的 TTL。如果你需要 TTL,请使用 TTL keyPTTL key 作为单独的命令。

GETEX 则不同:它返回值并同时更改键的过期时间。这对于滑动会话行为很有用,其中每次读取都会延长会话生命周期。

GETEX session:abc123 EX 1800

这会读取会话值并将过期时间重置为 30 分钟。不要随意将其用于正常的缓存读取,因为每次读取都变成了一个类似写入的操作,会更改键的元数据。

3. 实际应用:使用 GETSET 进行缓存

GETSET 的基本用例是实现简单的旁路缓存模式。

应用程序逻辑中的步骤:

  1. 尝试 GET product:500
  2. 如果 Redis 返回一个值,解码并返回它。
  3. 如果 Redis 返回 nil,从主数据库获取产品。
  4. 使用 SET product:500 <json> EX 300 存储序列化后的结果。
  5. 将结果返回给调用者。

这种模式可以减少数据库负载,但也会产生一个陈旧数据窗口。如果数据库中的产品发生了变化,Redis 可能仍然提供旧值,直到 TTL 过期或你的应用程序使该键失效。选择 TTL 时应基于数据允许的错误程度,而不仅仅是你想节省多少流量。

命名键而不制造混乱

Redis 不要求命名约定,但未来的你会需要。像 user:100:nameproduct:500:summaryrate:user:100:login 这样的可读模式使得使用 redis-cli 调试更加容易。

保持键清晰且合理简短。除非你在非常大的规模下运行并且已经测量了键的开销,否则通过将键命名为 u:100:n 来节省几个字节很少值得引起混淆。对于大多数团队来说,一致性比极端简洁更重要。

注意键中用户提供的值。如果电子邮件地址、URL 或租户名称成为键的一部分,请先对其进行规范化。否则,微小的格式差异可能会创建重复的缓存条目:

[email protected]
[email protected]
 [email protected]

这些对于你的应用程序来说可能代表同一个用户,但对于 Redis 来说却是不同的键。

覆盖、空值和 Nil

SET 默认会覆盖:

SET config:mode "safe"
SET config:mode "fast"
GET config:mode

最终值是 "fast"。如果覆盖有风险,请使用 NXXX

另外,要区分缺失的键和空值。Redis nil 表示键缺失。空字符串是实际存储的值:

SET user:100:nickname ""
GET user:100:nickname

你的客户端库可能以不同方式表示这些:nullNonenil、空字节字符串或空字符串。请检查客户端行为,而不是猜测。

安全锁模式及警告

你经常会看到这种模式:

SET lock:invoice:123 "worker-7:1700000000" NX EX 30

这意味着“仅当此锁不存在时创建它,并在 30 秒后过期。”过期时间不是可选的。没有它,崩溃的工作进程可能会永久留下一个锁。

对于简单的单实例 Redis 设置,这种模式通常足以用于低风险的协调。对于跨故障、时钟漂移和多个 Redis 节点的关键分布式锁,请使用经过良好审查的库并理解其权衡。锁错误可能变成数据损坏错误。

使用 redis-cli 调试

GETSET 路径行为异常时,直接检查键:

redis-cli GET product:500
redis-cli TTL product:500
redis-cli TYPE product:500

TYPE 很有用,因为 GET 只适用于字符串值。如果键持有哈希、列表、集合或有序集合,Redis 会返回类型错误。这通常意味着应用程序的两个部分正在为不同目的使用相同的键名。

如果你需要在开发期间检查几个相关的键,SCAN 比在繁忙的生产服务器上使用 KEYS 更安全:

redis-cli SCAN 0 MATCH 'product:500:*' COUNT 100

KEYS * 会在 Redis 扫描键空间时阻塞它。在小型本地实例上没问题。在生产环境中这是一个坏习惯。

在实际系统中选择 TTL

TTL 的选择是一个产品和运营决策,而不是 Redis 技巧。用户资料缓存可能容忍五分钟的陈旧。权限检查可能需要更短的 TTL 或显式失效。功能开关如果需要控制有风险的发布,可能需要近乎即时的更新。

以下是三种常见模式:

SET cache:product:500 "<json>" EX 300
SET session:abc123 "<json>" EX 1800
SET rate:user:100:login "1" EX 60 NX

产品缓存可以稍微陈旧。会话有明确的生存期。速率限制键使用 NX,以便第一次尝试创建窗口,后续尝试可以根据设计递增或检查相关键。

除非你有明确的失效路径,否则避免“永久缓存”键。永久缓存最终会成为第二个数据库,通常没有你对真实数据库应用的操作纪律。

序列化细节

Redis 字符串是二进制安全的。它们可以保存 JSON、MessagePack、压缩数据、计数器或纯文本。命令不关心这些。你的应用程序关心。

JSON 易于检查:

SET product:500 "{\"id\":500,\"name\":\"Desk Lamp\"}" EX 300

在某些应用程序中,二进制格式可以节省空间或 CPU,但它们会使终端调试更加困难。压缩有助于处理大型重复数据,但它也会增加 CPU 成本,并可能掩盖你正在缓存过大对象的事实。

对于计数器,如果多个客户端可能更新同一个键,请不要使用 GET 读取、在应用程序中添加、然后使用 SET 写入。请改用 Redis 原子计数器命令:

INCR page:view:500
EXPIRE page:view:500 86400

对于需要首次写入并设置过期时间的计数器,如果你需要严格的行为,请使用事务或小型 Lua 脚本。否则,请明确你接受的竞争条件。

避免键冲突

两个团队使用同一个 Redis 数据库可能会意外重用像 user:100 这样的键。一个团队使用 SET 存储 JSON;另一个团队使用 HSET 存储字段。下一个 GET 会返回类型错误,两个团队都会浪费时间。

命名空间有所帮助:

shop:prod:user:100:profile
shop:prod:session:abc123
billing:prod:invoice:9001

你不需要冗长的键,但要包含足够的上下文以避免环境、服务和数据类型之间的冲突。如果你跨应用程序共享 Redis,命名约定是接口的一部分。

何时不使用 GETSET

字符串是起点,而不是整个 Redis 模型。如果你经常更新一个较大的 JSON 对象中的某个字段,哈希可能更清晰:

HSET user:100 name "Alice Johnson" email "[email protected]"
HGET user:100 email

如果你需要有序事件,请使用流或列表。如果你需要成员资格检查,请使用集合。如果你需要排名,请使用有序集合。为每个小改动重写整个序列化字符串起初很简单,但随着对象的增长,它可能会变得昂贵且笨拙。

发布前的小清单

在发布新的 GETSET 路径之前,问几个简单的问题。

  • 确切的键名是什么?
  • 两个服务会意外使用同一个键吗?
  • 键应该过期吗?
  • 当 Redis 返回 nil 时会发生什么?
  • 覆盖是否可以接受,还是应该使用 NXXX
  • 值是否足够小,可以作为单个字符串读写?
  • 如果生产行为看起来不对,你能从 redis-cli 调试该值吗?

这些问题能在大多数基本的 Redis 字符串错误变成事故之前捕获它们。命令语法很容易。键周围的生命周期才是错误通常隐藏的地方。

GETSET 是小型命令,但具有很大的操作影响。为缓存数据使用过期时间,在覆盖重要时使用 NXXX,将 nil 视为单独的状态,并保持键名足够一致,以便有人可以从终端调试它们。一旦字符串感觉局促,将相关字段移到哈希中,并使用与访问模式匹配的数据结构。