- 后端
- 数据库客户端
- 缓存
【免费下载链接】go-redis
Redis Go client
导读
本文围绕 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),这就需要一个从"粗"到"细"逐级缩窄的流程:
- 先问:这批匹配的时序上,都有哪些标签名?
- 再问:选中的某个标签,在匹配时序上有哪些取值?
- 最后问:完整筛选条件下,具体是哪些时序键?
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
相关推荐
告别混乱时序:Grafana标签化注释查询实战指南
告别混乱时序:Grafana标签化注释查询实战指南 你是否还在为时间序列数据中的关键事件标记而烦恼?当系统故障发生时,能否快速定位到同时段的异常指标?Grafa
可观测性指标监控数据可视化告警日志分析后端前端Lightdash Data App `drillDown()` API 实战指南:从点击行构建下钻查询
Lightdash Data App drillDown API 实战指南:从点击行构建下钻查询 drillDown 是 Lightdash Query SDK
后端前端数据分析数据可视化人工智能AI AgentIT-Tools 加密解密实战:HMAC 生成、JWT 解析与 RSA 密钥三个场景讲明白
IT Tools 加密解密实战:HMAC 生成、JWT 解析与 RSA 密钥三个场景讲明白 接口签名死活对不上,却不知道是明文、密钥还是编码的问题?拿到一串 J
开发工具前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考