Redis MGET 入门:批量获取数据的命令 – wiki基地


Redis MGET 入门:批量获取数据的效率之道

在现代互联网应用中,数据的高效存取是决定系统性能的关键因素之一。作为高性能的键值存储系统,Redis 因其极快的读写速度而广受欢迎,常被用作缓存、消息队列、分布式锁等。在使用 Redis 进行数据读取时,我们最常用的命令是 GET,它用于获取单个键的值。然而,在许多场景下,我们需要同时获取多个键的值,如果简单地循环使用 GET 命令,就会带来显著的性能问题。

这时,Redis 的 MGET 命令就闪亮登场了。MGET(Multi GET)命令允许我们在一次操作中获取多个键的值,极大地提高了数据批量读取的效率。本文将带你深入了解 MGET 命令,从基础用法到其带来的性能优势,以及在实际应用中的注意事项,帮助你掌握这一重要的 Redis 命令。

为什么需要批量获取数据?单个 GET 不够吗?

在探讨 MGET 之前,我们先来看看为什么循环使用 GET 命令会存在效率问题。

想象一下这样的场景:你需要从 Redis 中获取一个用户的多种信息,例如用户的 ID、用户名、注册时间、最后登录时间、积分等。这些信息可能分别存储在不同的 Redis 键中(例如 user:123:id, user:123:username, user:123:reg_time 等)。

如果使用循环来逐个 GET

GET user:123:id
GET user:123:username
GET user:123:reg_time
...

每次 GET 操作都需要经历以下几个步骤:

  1. 客户端发送命令: 应用程序(客户端)将 GET 命令发送到 Redis 服务器。
  2. 网络传输: 命令通过网络从客户端传输到服务器。
  3. 服务器处理: Redis 服务器接收到命令,查找对应的键,获取值。
  4. 网络传输: 服务器将结果通过网络传输回客户端。
  5. 客户端接收结果: 应用程序接收并处理结果。

这个过程称为一个 网络往返(Round Trip)。对于每一个 GET 命令,都需要完成一次网络往返。如果需要获取 N 个键的值,那么就需要进行 N 次网络往返。

网络往返的开销主要体现在两个方面:

  1. 网络延迟(Latency): 即使网络带宽很高,数据包在网络中传输仍然存在延迟。这个延迟可能由物理距离、网络拥堵、路由器转发等因素引起。每次往返都需要等待这个延迟。
  2. 服务器上下文切换: 服务器在处理每个独立的命令时,都需要进行一些准备和清理工作,例如解析命令、查找客户端连接、发送响应等。虽然 Redis 内部是单线程处理命令(核心命令处理部分),但处理多个独立的请求仍然会产生一定的开销。

当 N 比较大时(比如几十个、几百个甚至更多),N 次网络往返累积起来的总延迟将非常可观,成为影响应用程序响应速度的瓶颈。这就像你去超市购物,不是一次把所有商品放进购物车结账,而是每拿一个商品就排一次队结账,效率自然低下。

MGET 命令正是为了解决这个问题而设计的。 它允许你在一次网络往返中请求获取多个键的值,极大地减少了网络通信的次数,从而显著提升批量读取的效率。

初识 Redis MGET 命令

MGET 命令用于获取所有给定键的值。如果某个键不存在,那么在结果列表中对应位置的值将是特殊值 nil(在不同的客户端库中可能表示为 nullNone 或一个特定的空值对象)。

命令语法:

MGET key [key ...]

你可以向 MGET 命令传递任意数量的键名作为参数。

返回值:

MGET 命令返回一个列表(List 或 Array),其中包含了所有给定键的值。这个列表的元素的顺序与你在命令中指定键的顺序是完全一致的。列表中每个元素对应输入参数中的一个键:

  • 如果对应的键存在且存储的是字符串类型(string),则返回该字符串的值。
  • 如果对应的键存在但存储的是非字符串类型,Redis 会返回一个错误(不过通常 MGET 只用于字符串类型键)。
  • 如果对应的键不存在,则返回 nil

示例:

假设我们在 Redis 中有以下几个键:

rediscli
SET name "Alice"
SET age "30"
SET city "New York"
-- hobby 不存在

现在我们使用 MGET 命令同时获取 name, age, hobby, city 这四个键的值:

rediscli
MGET name age hobby city

Redis 服务器将返回一个列表:

1) "Alice"
2) "30"
3) (nil)
4) "New York"

可以看到,返回的列表顺序与输入的键顺序一致,且 hobby 键不存在,其对应的值为 (nil)

MGET 的实际操作演练

我们使用 redis-cli 来实际操作 MGET 命令,以便更好地理解其用法和返回值。

首先,确保你的 Redis 服务器正在运行。然后打开终端,输入 redis-cli 连接到服务器。

步骤 1:设置一些键值对

我们先设置几个键,其中包含不同类型的值和不存在的键:

“`rediscli
SET user:profile:1:name “Bob”
SET user:profile:1:email “[email protected]
SET user:profile:1:age “25”
SET product:info:1001:name “Laptop”
SET product:info:1001:price “1200”
SET user:profile:2:name “Charlie”

— 以下键我们不设置,模拟不存在的情况
— user:profile:1:phone
— product:info:1002:name
“`

步骤 2:使用 MGET 获取存在的键

现在,我们使用 MGET 获取用户 1 的姓名、邮箱和年龄:

rediscli
MGET user:profile:1:name user:profile:1:email user:profile:1:age

预期输出:

1) "Bob"
2) "[email protected]"
3) "25"

结果是一个包含三个元素的列表,分别对应了输入的三个键的值,顺序也一致。

步骤 3:使用 MGET 获取部分存在、部分不存在的键

这次,我们获取用户 1 的姓名、电话号码(不存在)以及产品 1001 的价格:

rediscli
MGET user:profile:1:name user:profile:1:phone product:info:1001:price

预期输出:

1) "Bob"
2) (nil)
3) "1200"

正如所料,user:profile:1:phone 不存在,其对应位置返回了 (nil)。这再次强调了 MGET 返回结果的顺序性以及对不存在键的处理方式。

步骤 4:使用 MGET 获取全部不存在的键

如果我们尝试获取几个都不存在的键:

rediscli
MGET non_existent_key_1 non_existent_key_2

预期输出:

1) (nil)
2) (nil)

两个键都不存在,返回的列表包含两个 (nil)

通过这些示例,你应该已经掌握了 MGET 命令的基本用法以及如何解读其返回值。在编写应用程序代码时,你需要根据这个返回值列表来处理数据,特别是要考虑到 nil 的情况。

MGET 带来的性能优势深入分析

现在我们更深入地分析一下 MGET 为什么能带来显著的性能提升。

核心原因在于 减少网络往返次数。假设客户端与服务器之间的网络延迟是 L 毫秒。

  • 使用 N 个 GET 命令: 总耗时大致为 N * (网络往返时间 + 服务器处理单个命令时间)。网络往返时间通常占据主导地位,所以总耗时接近 N * L
  • 使用 1 个 MGET 命令: 总耗时大致为 1 * (网络往返时间 + 服务器处理 MGET 命令时间)。网络往返时间仍然是 L,但服务器处理 MGET 命令的时间虽然比处理单个 GET 要长,但通常远小于处理 N 个 GET 命令的总时间。服务器处理 MGET 主要是迭代查找 N 个键的值,这通常非常快。所以总耗时接近 L + 服务器处理 MGET 的时间

对比 N * LL + ...,当 N 较大时,MGET 的优势就非常明显了。它将 N 次网络往返“压缩”成了 1 次。

举个例子:如果网络延迟是 1 毫秒,需要获取 100 个键。

  • 100 个 GET:至少需要 100 * 1 ms = 100 ms 的网络等待时间,加上服务器处理时间。
  • 1 个 MGET:只需要 1 * 1 ms = 1 ms 的网络等待时间,加上服务器处理时间。

即使服务器处理 100 个键的 MGET 命令可能需要几毫秒,总时间也远低于 100 毫秒。

此外,减少网络连接的建立和关闭、减少客户端和服务器的网络I/O操作次数,也能在一定程度上减轻系统资源的消耗。

因此,无论是在高并发场景下提升吞吐量,还是在低延迟场景下减少响应时间,使用 MGET 批量获取数据都是一个非常有效的优化手段。

实际应用场景举例

MGET 命令在各种应用场景中都非常有用:

  1. 用户资料缓存: 当用户登录或访问个人主页时,可能需要同时加载用户的基本信息(昵称、头像、等级)、联系方式、偏好设置等。这些信息可以分别存储在 Redis 的不同键中(例如 user:123:name, user:123:avatar, user:123:level, user:123:contact, user:123:prefs)。使用 MGET 一次性获取这些键的值,可以显著加快用户资料的加载速度。
  2. 商品信息展示: 在电商网站的商品详情页,除了商品的基本信息(名称、价格、描述)外,还可能需要显示库存、促销信息、运费模板等。这些信息同样可以存储在多个 Redis 键中(例如 product:1001:name, product:1001:price, product:1001:stock, product:1001:promo, product:1001:shipping_tmpl)。使用 MGET 可以快速地将这些信息从缓存中取出并展示给用户。
  3. 配置参数读取: 应用程序启动时或运行时可能需要加载多个配置参数。如果这些参数存储在 Redis 中,使用 MGET 可以一次性加载所需的全部配置,避免多次网络请求。
  4. 排行榜数据: 如果排行榜数据存储在多个 Sorted Set 或 List 中,但需要获取与排行榜相关的其他信息(例如用户的昵称、头像等,这些可能存在 String 类型的键中),在获取 Sorted Set/List 数据后,可以使用 MGET 根据用户 ID 批量获取用户的附加信息。

总之,任何需要同时获取多个独立但相关的数据项的场景,都应该考虑使用 MGET 来代替多次 GET

使用 MGET 的注意事项与最佳实践

虽然 MGET 非常高效,但在使用时也需要考虑一些因素:

  1. 批量大小的权衡: MGET 命令的参数数量是有限制的,虽然限制很高(理论上可以接受非常多的键),但在实际应用中,一次性获取的键数量不宜过多。原因如下:

    • 服务器内存消耗: MGET 命令需要在服务器端构建一个包含所有结果的响应列表,这会占用一定的内存。如果一次获取的键太多,尤其是值比较大时,可能会对服务器内存造成瞬时压力。
    • 网络带宽: 虽然减少了往返次数,但单个响应的数据量变大了。如果批量获取的值总量非常大,可能会占用较多的网络带宽,甚至导致 TCP 缓冲区溢出等问题。
    • 服务器处理时间: 处理单个 MGET 命令的时间与键的数量大致呈线性关系。键太多可能导致该命令执行时间过长,阻塞 Redis 单线程模型的其他命令处理,尤其是在键值较大时。
    • 客户端接收和处理: 客户端需要接收并解析一个很大的响应,这也会消耗客户端的资源。

    建议: 实践中,一次 MGET 获取几十个、几百个或一两千个键通常是比较合适的。具体的最佳批量大小取决于你的网络环境、Redis 服务器的性能、键值的大小分布以及应用对延迟的要求。你可以通过测试来找到适合你场景的最佳批量大小。如果需要获取的键非常多(比如几万几十万),应该分批进行 MGET 操作。

  2. 处理 nil 值: MGET 返回结果中的 nil 值表示对应的键不存在。在应用程序代码中,必须正确地处理这些 nil 值。例如,如果获取用户资料,某个字段的键不存在,你的代码应该能够识别出对应的结果是 nil,并给出合理的默认值或者标记为缺失。

  3. 原子性: MGET 命令是一个原子操作(在 Redis 服务器单线程执行命令的上下文中)。这意味着当你执行 MGET 时,Redis 会在执行期间一次性地读取所有指定的键的值,不会被其他客户端的命令打断。因此,你获取到的是这些键在命令执行那一瞬间的一个一致性快照。但是,这并不意味着 MGET 是一个事务。如果在 MGET 执行前后有其他客户端修改了其中的某个键,MGET 获取的是修改之前或修改之后的值,取决于哪个操作先到达服务器并执行。如果你需要更强的原子性(例如,在获取一批键的同时,根据这些键的值做一些判断并修改其他键),你应该考虑使用 Redis 事务(MULTI/EXEC)或者 Lua 脚本。

  4. 键的类型: MGET 命令主要用于获取 String 类型的键的值。如果你尝试对非 String 类型的键使用 MGET,Redis 会返回错误。在设计数据存储时,确保你计划通过 MGET 获取的键都是 String 类型。

  5. 客户端库的支持: 几乎所有的主流 Redis 客户端库都提供了对 MGET 命令的良好支持,通常会封装成一个方便的方法,接收一个键名列表作为参数,并返回一个值列表。使用这些客户端库可以让你更便捷地在应用程序中使用 MGET。例如,在 Python 的 redis-py 库中,你可以这样做:

    “`python
    import redis

    r = redis.StrictRedis(host=’localhost’, port=6379, db=0)

    Set some keys

    r.set(‘key1’, ‘value1’)
    r.set(‘key2’, ‘value2’)
    r.set(‘key4’, ‘value4’) # key3 will not be set

    Use MGET

    keys_to_get = [‘key1’, ‘key2’, ‘key3’, ‘key4’]
    results = r.mget(keys_to_get)

    print(results)

    Expected output (list of bytes, depends on client configuration):

    [b’value1′, b’value2′, None, b’value4′]

    Process results, remember None/nil handling

    for key, value in zip(keys_to_get, results):
    if value is not None:
    print(f”Key ‘{key}’ found, value: {value.decode()}”) # Decode bytes if necessary
    else:
    print(f”Key ‘{key}’ not found”)
    “`

    注意客户端库返回 nil 的表示方式(通常是 Nonenull)。

  6. 与 Pipeline 的比较: Redis 还提供了 Pipeline(管道)机制,它允许客户端一次性发送多个命令给服务器,服务器依次执行这些命令,然后一次性将所有结果返回给客户端。这也能减少网络往返次数。MGET 可以看作是针对批量获取 String 类型键值的一种特殊且非常高效的管道操作(只包含一个 MGET 命令)。如果你需要批量执行不同类型的命令(例如 GET, SET, LPUSH, ZADD 等),或者批量执行多个 独立的 GET 命令而不是一个 MGET,那么 Pipeline 是更通用的选择。但对于仅仅批量获取多个 String 键值的场景,使用 MGET 通常是更简洁和直接的方式,且其内部实现可能比使用 Pipeline 发送多个 GET 命令更优化一些(例如,服务器在处理单个 MGET 时可能更高效地处理键的查找和结果的构建)。

总结

Redis MGET 命令是批量获取多个键值的高效工具。通过将多个独立的 GET 请求合并成一个 MGET 请求,它显著减少了客户端与服务器之间的网络往返次数,从而降低了网络延迟,提高了应用程序的响应速度和吞吐量。

掌握 MGET 命令的基本用法、了解其返回值的特性(特别是对 nil 的处理)、认识其带来的性能优势,并在实际应用中结合批量大小的权衡、错误处理等最佳实践,能够让你更有效地利用 Redis 强大的数据读取能力,构建出高性能、高可用的应用系统。

当你发现代码中存在循环执行多个 GET 命令的情况时,停下来思考一下,是否可以使用 MGET 来进行优化?很可能,这将为你带来意想不到的性能提升。

通过本文的学习,相信你已经对 Redis 的 MGET 命令有了全面的认识,并能够在今后的开发中 confidently 使用它来优化你的数据读取流程。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部