☰
go-redis 实战:用 TS.QUERYLABELS 实现时序标签下钻查询(TSQueryLabels / TSQueryLabelValues)
2026/10/1 9:30:15 网站建设 项目流程
  • 后端
  • 数据库客户端
  • 缓存

【免费下载链接】go-redis

Redis Go client

项目地址:https://gitcode.com/GitHub_Trending/go/go-redis
点击查看免费下载

导读

本文围绕 go-redis 官方示例 example/ts-querylabels 展开,系统讲解 Redis 8.10+ TimeSeries 模块新增的TS.QUERYLABELS命令在 go-redis 中的两种绑定形式:TSQueryLabels(查询一组匹配时序上出现的全部标签名)与TSQueryLabelValues(查询某个标签在匹配时序上取到的全部值)。读完本文,你将掌握用 go-redis 构建"标签名 → 标签值 → 时序键"三级下钻(drill-down)流程的完整写法,理解客户端如何构造命令参数、服务器返回语义有哪些边界行为,并看懂对应的单元测试与集成测试如何钉死命令的线上格式(wire format)。

背景:标签发现为何重要

RedisTimeSeries 中的每条时序(time series)都可以携带一组标签(label),例如{type: sensor, location: kitchen, unit: celsius}。标签是时序检索的核心索引:TS.QUERYINDEX可以返回匹配某个过滤表达式的全部时序键,TS.MRANGE/TS.MGET可以跨多组时序聚合查询。

但在"给用户一个查询界面"的真实场景中,用户并不预先知道系统里存在哪些标签、某个标签有哪些可选值。仪表盘工具(如 Grafana)需要在运行时动态填充下拉框变量(variable dropdowns),这就需要一个从"粗"到"细"逐级缩窄的流程:

  1. 先问:这批匹配的时序上,都有哪些标签名?
  2. 再问:选中的某个标签,在匹配时序上有哪些取值?
  3. 最后问:完整筛选条件下,具体是哪些时序键?

Redis 8.10+ 的TS.QUERYLABELS正好补齐了前两步,与既有的TSQueryIndex一起构成完整的下钻闭环。go-redis 在 timeseries_commands.go 中为其提供了两个绑定方法,并配套了官方示例与测试。

命令的两种形式与 Go 绑定

TS.QUERYLABELS有两种形式,对应 go-redis 两个方法(接口声明见 timeseries_commands.go):

命令形式go-redis 方法语义
TS.QUERYLABELS LABELS [FILTER expr ...]TSQueryLabels(ctx, filterExpr)返回匹配时序上出现过的全部标签名集合
TS.QUERYLABELS VALUES label [FILTER expr ...]TSQueryLabelValues(ctx, label, filterExpr)返回指定标签在匹配时序上取到的全部值集合

两个方法都返回*StringSliceCmd,即一组字符串;服务器端返回的是无序、已去重的集合,客户端不做额外排序或去重。

过滤表达式的传递规则

filterExpr使用的是与TSQueryIndex、TSMRange完全相同的过滤表达式语言(如type=sensor、location!=kitchen、l=(a,b)等),并且原样透传(verbatim)给服务器,客户端不做任何解析、校验或改写。这意味着:

  • 传入nil或空切片[]string{}表示不附加任何过滤条件,查询数据库内所有已建立索引的时序;
  • 传入多个表达式时,顺序原样保留,由服务器端负责解释。

空过滤器必须省略 FILTER 关键字

一个容易踩坑的实现细节是:当过滤表达式为空时,客户端必须省略FILTER关键字,因为服务器会拒绝一个光秃秃的FILTER。go-redis 通过内部辅助函数 appendTSFilter 处理这一点:

func appendTSFilter(args []interface{}, filterExpr []string) []interface{} { if len(filterExpr) == 0 { return args } args = append(args, "FILTER") for _, f := range filterExpr { args = append(args, f) } return args }

运行示例:三步下钻 + 两个边界场景

官方示例位于 example/ts-querylabels/main.go,演示了一个完整的仪表盘下钻流程。先创建 4 条带标签的时序作为种子数据:

series := map[string]map[string]string{ "ts:temp:kitchen": {"type": "sensor", "location": "kitchen", "unit": "celsius"}, "ts:temp:bedroom": {"type": "sensor", "location": "bedroom", "unit": "celsius"}, "ts:hum:kitchen": {"type": "sensor", "location": "kitchen"}, "ts:build:count": {"type": "counter"}, }

注意ts:hum:kitchen故意没有unit标签,ts:build:count只有type标签——它们正是用来演示边界行为的。

第 1 步:第一个下拉框——标签名

labels, err := rdb.TSQueryLabels(ctx, []string{"type=sensor"}).Result() fmt.Printf("label names for type=sensor: %v\n", labels)

过滤出所有type=sensor的时序,返回它们携带的标签名集合。示例输出:

label names for type=sensor: [location type unit]

两个关键点(示例注释与源码注释一致):返回无序且去重;包含过滤条件本身用到的标签名(这里就是type)。

第 2 步:第二个下拉框——标签值

locations, err := rdb.TSQueryLabelValues(ctx, "location", []string{"type=sensor"}).Result() fmt.Printf("locations for type=sensor: %v\n", locations)

在type=sensor的时序组里,查询location标签的所有取值。输出:

locations for type=sensor: [bedroom kitchen]

没有location标签的时序(本组内不存在此情况,但见第 3 步的unit)对该集合不产生任何贡献。

第 3 步:第三个下拉框——时序键

下钻到最终条件后,用既有的TSQueryIndex拿具体时序键:

series, err := rdb.TSQueryIndex(ctx, []string{"type=sensor", "location=kitchen"}).Result() fmt.Printf("kitchen sensor series: %v\n", series)

输出:

kitchen sensor series: [ts:hum:kitchen ts:temp:kitchen]

至此,"标签名 → 标签值 → 时序键"三级下钻完成。TSQueryIndex的实现见 timeseries_commands.go,与TS.QUERYLABELS共用同一套过滤表达式语言。

边界场景 A:不存在的标签返回空集合而非错误

missing, err := rdb.TSQueryLabelValues(ctx, "rack", []string{"type=sensor"}).Result() fmt.Printf("values of an absent label: %d\n", len(missing))

没有任何匹配时序携带rack标签时,返回空回复(empty reply)而不是错误,输出0。这对仪表盘很关键:某个标签值暂时为空时,界面照常工作,不会中断渲染。

边界场景 B:无过滤条件查询全部

all, err := rdb.TSQueryLabels(ctx, nil).Result() fmt.Printf("label names, all series: %v\n", all)

不传任何过滤器,查询数据库内所有已建立索引的时序。示例输出:

label names, all series: [location type unit]

(注意这里没有rack等标签,因为种子数据中根本不存在。)

运行方式与环境要求

示例模块声明见 example/ts-querylabels/go.mod,其中replace github.com/redis/go-redis/v9 => ../..让示例直接指向仓库根目录的 go-redis 源码。运行前需要:

  • Redis 8.10+并加载 TimeSeries 模块;
  • 服务监听在localhost:6379(代码中的redis.Options{Addr: "localhost:6379"})。

进入示例目录后直接运行:

go run .

预期输出(集合内部顺序可能不同):

label names for type=sensor: [location type unit] locations for type=sensor: [bedroom kitchen] kitchen sensor series: [ts:hum:kitchen ts:temp:kitchen] values of an absent label: 0 label names, all series: [location type unit]

示例代码在main开头先seed建时序、结束前rdb.Del(ctx, keys...)清理,保证可重复运行。

源码级剖析:命令构造与线上格式

两个方法的完整实现(timeseries_commands.go):

func (c cmdable) TSQueryLabels(ctx context.Context, filterExpr []string) *StringSliceCmd { args := []interface{}{"TS.QUERYLABELS", "LABELS"} args = appendTSFilter(args, filterExpr) cmd := NewStringSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd } func (c cmdable) TSQueryLabelValues(ctx context.Context, label string, filterExpr []string) *StringSliceCmd { args := []interface{}{"TS.QUERYLABELS", "VALUES", label} args = appendTSFilter(args, filterExpr) cmd := NewStringSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd }

由此可以推断出实际发往服务器的参数序列:

调用生成的参数
TSQueryLabels(ctx, nil)TS.QUERYLABELS LABELS
TSQueryLabels(ctx, []string{"type=sensor"})TS.QUERYLABELS LABELS FILTER type=sensor
TSQueryLabelValues(ctx, "location", []string{"type=sensor"})TS.QUERYLABELS VALUES location FILTER type=sensor
TSQueryLabelValues(ctx, "location", nil)TS.QUERYLABELS VALUES location

单元测试:钉死 wire format

timeseries_querylabels_unit_test.go 中的TestTSQueryLabelsArgs用captureCmdable捕获命令参数,逐条比对期望值,重点验证了四件事:

  • FILTER令牌的省略规则:nil与空切片都生成不带FILTER的参数(labels_without_filter_omits_filter_token、labels_with_empty_filter_slice_omits_filter_token);
  • 过滤器原样、有序传递:[]string{"type=sensor", "location!=", "l=(a,b)"}原样出现在参数中(labels_filters_verbatim_and_ordered);
  • VALUES 形式同样遵循省略规则(values_without_filter_omits_filter_token、values_with_filters);
  • 标签名不做任何规范化:TSQueryLabelValues(ctx, " LoCaTiOn ", nil)生成的参数就是TS.QUERYLABELS VALUES LoCaTiOn——前导与尾随空格、大小写原样保留(values_label_not_normalized)。

最后这一点与集成测试中的"字节精确匹配"遥相呼应:标签名按字节精确匹配,Location与location是两个不同的标签。

集成测试:RESP2 / RESP3 下的行为验证

timeseries_querylabels_integration_test.go 用 Ginkgo/Gomega 在 RESP2 与 RESP3 两种协议下各跑一遍,通过SkipBeforeRedisVersion("8.10", ...)在低于 8.10 的服务器上自动跳过,并用FlushDB+ 组内唯一过滤值(test_group=querylabels-test-respN)隔离数据。它验证的行为与示例 README 的关键点一一对应:

  • 标签名集合包含过滤条件中的标签名:ConsistOf("test_group", "location", "unit");
  • 标签值集合去重、缺标签的时序不贡献:location得到kitchen、bedroom两个值,而只有部分时序携带的unit只得到celsius一个值;
  • 不存在的标签返回空回复而非错误:TSQueryLabelValues(ctx, "no-such-label", ...)期望BeEmpty();
  • 标签名字节精确匹配:查询"Location"(大写变体)得到空集合;
  • 省略过滤器查询全部已索引时序:在FlushDB后的空库中,TSQueryLabels(ctx, nil)恰好返回本组三条时序的标签集合;
  • 服务端过滤错误原样传播:传一个只有非包含匹配(test_group!=...)的过滤器时,错误由服务器返回且包含TSDB:前缀——客户端不参与语法校验,印证了"过滤器原样透传"的设计。

这套测试同时覆盖了 RESP2 的数组回复与 RESP3 的集合回复,说明两个方法在两种协议下的返回类型(*StringSliceCmd)都能正确解析。

小结

TS.QUERYLABELS是 Redis 8.10+ TimeSeries 为仪表盘下钻场景补齐的关键拼图。go-redis 通过TSQueryLabels与TSQueryLabelValues两个方法封装了它的两种形式,加上既有的TSQueryIndex,可以构建出"标签名 → 标签值 → 时序键"的完整变量下钻流程。使用时要记住三点:过滤器与TSQueryIndex/TSMRange共用同一套表达式语言并被原样透传;回复是无序去重、包含过滤标签名的字符串集合;标签缺失或查询无结果返回空回复而非错误,因此仪表盘逻辑可以放心地把"空"当作合法的界面状态。

想要深入验证行为,可以阅读仓库中的 单元测试 与 集成测试;想要直接跑起来观察输出,参考 示例 README 与 main.go。

  • 后端
  • 数据库客户端
  • 缓存

【免费下载链接】go-redis

Redis Go client

项目地址:https://gitcode.com/GitHub_Trending/go/go-redis
点击查看免费下载

相关推荐

上一篇:BootstrapCDN by jsDelivr
下一篇:【亲测免费】 cron-parser: 解析Unix Cron表达式的强大工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询