在Redis的五大基础数据类型中,哈希类型特别适合存储对象的属性集合,比如用户资料、商品详情、配置项等。当我们需要从一个哈希键中读取多个字段时,如果逐个调用HGET命令,每次请求都会产生一次网络往返,性能开销相当可观。Redis提供的HMGET命令正是为了解决这个问题,它支持在一次请求中批量获取同一个哈希中多个指定字段的值,是哈希读取场景中性价比最高的命令之一。

HMGET命令的基本语法与返回值规则
HMGET的语法结构非常简单:HMGET key field [field ...]。也就是先指定哈希键名,后面跟上若干个字段名,字段数量至少为一个,理论上没有上限,但实际使用中要受到单条命令大小和客户端缓冲区的限制。举个直接的例子,假设我们用哈希存储了用户ID为1001的资料,键名为user:1001,包含name、age、city三个字段,那么执行HMGET user:1001 name age city就能一次性拿到这三个字段的值。
关于返回值,有几点值得特别注意。HMGET返回的是一个列表,列表中元素的顺序与请求字段的顺序严格一一对应,这一点在做业务映射时非常关键。如果某个字段在哈希中不存在,对应位置返回的是nil,而不是报错;如果整个哈希键本身都不存在,Redis不会抛出异常,而是返回一个全部为nil的列表,列表长度仍然等于请求字段的数量。这个设计保证了调用方不需要额外判断键是否存在,直接按位置解析即可。
我们可以在redis-cli里做一组对比实验来加深理解:
127.0.0.1:6379> HSET user:1001 name tom age 25 (integer) 2 127.0.0.1:6379> HMGET user:1001 name age city 1) "tom" 2) "25" 3) (nil) 127.0.0.1:6379> HMGET user:2000 name age 1) (nil) 2) (nil)
可以看到,city字段不存在返回nil,而完全不存在的键user:2000返回的也只是两个nil。另外需要注意,HMGET只能作用于哈希类型,如果对字符串或列表类型的键执行HMGET,会返回WRONGTYPE错误,这一点在排查线上问题时经常遇到。
HMGET与HGETALL、HSCAN的选型对比
读取哈希数据的常用命令不止HMGET一个,HGETALL、HSCAN、HRANDFIELD等都能拿到字段和值,但它们的适用场景差异很大。HGETALL会返回哈希中所有字段和值,适合字段总数少且都需要读取的场景,比如一个配置对象只有十来个属性。但如果哈希中有几万个字段,而你只需要其中几十个,用HGETALL就会产生大量无用的数据传输,既浪费网络带宽,也增加Redis自身的序列化开销,这时候HMGET按需读取的优势就非常明显了。
HSCAN则用于渐进式遍历,适合字段数量巨大的哈希做全量扫描。它的特点是分批返回,每次调用通过游标推进,不会像HGETALL那样一次性阻塞Redis。如果你的需求是获取一个大哈希的全部内容,HSCAN比HGETALL更安全;如果只是获取指定的若干字段,HMGET依然是首选,因为它的时间复杂度是O(N),N为请求字段数,且都是O(1)的字典查找。
| 命令 | 返回内容 | 适用场景 | 风险点 |
|---|---|---|---|
| HGET | 单个字段值 | 只取一个字段 | 多次调用网络开销大 |
| HMGET | 指定字段值列表 | 批量取已知字段 | 字段过多时请求包变大 |
| HGETALL | 全部字段和值 | 小哈希全量读取 | 大哈希会阻塞和撑爆带宽 |
| HSCAN | 分批的字段和值 | 大哈希遍历 | 需处理游标逻辑 |
实际选型时可以遵循这样一个原则:字段已知且数量可控就用HMGET,字段未知或需要全量就用HSCAN,哈希很小且几乎每次都要全部数据才考虑HGETALL。把大而全的读取习惯改掉,往往就能解决一大类Redis慢查询问题。
代码实战:在业务中正确使用HMGET
以Go语言为例,使用go-redis客户端批量获取用户哈希的多个字段,代码大致如下:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
})
ctx := context.Background()
// 一次性获取多个字段,返回值顺序与请求顺序一致
vals, err := rdb.HMGet(ctx, "user:1001", "name", "age", "city").Result()
if err != nil {
panic(err)
}
for i, v := range vals {
if v == nil {
fmt.Printf("字段 %s 不存在\n", []string{"name", "age", "city"}[i])
} else {
fmt.Printf("字段值: %v\n", v)
}
}
}Java开发者常用的Jedis和Lettuce同样对HMGET有良好支持。以Lettuce为例,通过sync方式的hmget方法传入键和字段列表即可,返回的List中不存在的字段对应位置为null,解析逻辑和命令行行为完全一致。
RedisCommands<String, String> sync = client.connect().sync();
List<String> values = sync.hmget("user:1001", "name", "age", "city");
// values.get(0) 对应 name,get(1) 对应 age,以此类推
for (int i = 0; i < values.size(); i++) {
System.out.println(i + " -> " + values.get(i));
}在真实业务中还有几个值得留意的实践细节。首先是字段数量控制,虽然HMGET可以传入几百上千个字段,但单次请求体积过大会增加网络传输和解析耗时,一般建议单次控制在几十到一两百个字段,超出时分组多次请求,配合Pipeline或并发调用。其次是空值处理,业务层要对nil做好兜底,避免把nil直接反序列化导致空指针异常。最后,如果存在Redis Cluster环境,HMGET的所有字段必须在同一个哈希槽内,由于它们属于同一个键,这一点天然满足,但如果想对多个不同用户的哈希做批量读取,就需要用MGET配合键级hash tag,或者使用Pipeline分片发送。
总结一下,HMGET是哈希读取场景中最容易被低估的命令,它用一次网络往返替换了N次HGET调用,配合合理的字段拆分和空值处理,可以让对象读取类业务在Redis层面的性能表现提升一个量级。掌握它,并理解它与HGETALL、HSCAN之间的边界,是写出高质量Redis访问代码的基本功。
Redis HMGET哈希类型批量操作修改时间:2026-09-15 20:56:15