时间序列
提示
来自deepseek解释
原文链接:https://redis.io/docs/latest/develop/data-types/timeseries/
代码示例说明
下方的代码示例展示如何在不同的编程语言和客户端库中执行相同的操作:
- Redis CLI:Redis 命令行界面
- C# (同步):StackExchange.Redis 同步客户端
- C# (异步):StackExchange.Redis 异步客户端
- Go:go-redis 客户端
- Java (同步 - Jedis):Jedis 同步客户端
- Java (异步 - Lettuce):Lettuce 异步客户端
- Java (响应式 - Lettuce):Lettuce 响应式/流式客户端
- JavaScript (Node.js):node-redis 客户端
- PHP:Predis 客户端
- Python:redis-py 客户端
- Rust (同步):redis-rs 同步客户端
- Rust (异步):redis-rs 异步客户端
每个代码示例均以不同语言展示相同的基本操作。具体语法和模式会因语言和客户端库而异,但底层的 Redis 命令和行为保持一致。
时间序列命令摘要
本组共 17 条命令:
| 命令 | 摘要 | 复杂度 | 引入版本 |
|---|---|---|---|
| TS.ADD | 向时间序列追加一个样本 | O(M),M 为压缩规则数量... | 1.0.0 |
| TS.ALTER | 更新现有时间序列的保留期、块大小、重复策略和标签 | O(N),N 为请求的标签数量... | 1.0.0 |
| TS.CREATE | 创建一个新的时间序列 | O(1) | 1.0.0 |
| TS.CREATERULE | 创建压缩规则 | O(1) | 1.0.0 |
| TS.DECRBY | 减少具有最大时间戳的样本值,或使用给定减量创建新样本 | O(M),M 为压缩规则数量... | 1.0.0 |
| TS.DEL | 删除给定时间序列中两个时间戳之间的所有样本 | O(N),N 为被删除的数据点数... | 1.6.0 |
| TS.DELETERULE | 删除压缩规则 | O(1) | 1.0.0 |
| TS.GET | 从给定时间序列中获取具有最高时间戳的样本 | O(1) | 1.0.0 |
| TS.INCRBY | 增加具有最大时间戳的样本值,或使用给定增量创建新样本 | O(M),M 为压缩规则数量... | 1.0.0 |
| TS.INFO | 返回时间序列的信息和统计信息 | O(1) | 1.0.0 |
时间序列命令摘要(第2部分)
| 命令 | 摘要 | 复杂度 | 引入版本 |
|---|---|---|---|
| TS.MADD | 向一个或多个时间序列追加新样本 | O(N*M),N 为更新的序列数... | 1.0.0 |
| TS.MGET | 从匹配特定过滤器的每个时间序列中获取具有最高时间戳的样本 | O(n),n 为匹配的时间序列数... | 1.0.0 |
| TS.MRANGE | 按正向方向通过过滤器查询多个时间序列的范围 | O(n/m+k),n=数据点数,m=... | 1.0.0 |
| TS.MREVRANGE | 按反向方向通过过滤器查询多个时间序列的范围 | O(n/m+k),n=数据点数,m=... | 1.4.0 |
| TS.QUERYINDEX | 获取匹配过滤器列表的所有时间序列键 | O(n),n 为匹配的时间序列数... | 1.0.0 |
| TS.RANGE | 按正向方向查询范围 | O(n/m+k),n=数据点数,m=... | 1.0.0 |
| TS.REVRANGE | 按反向方向查询范围 | O(n/m+k),n=数据点数,m=... | 1.4.0 |
Redis 时间序列数据类型允许您存储带有采集时间的实数值数据点。 您可以组合来自所选时间序列的值,并按时间或值范围进行查询。您还可以计算一段时间内的数据聚合器,并从结果中创建新的时间序列。创建时间序列时,您可以指定数据的最大保留期限(相对于最后报告的时间戳),以防止时间序列无限增长。
从 Redis 8.6 开始,时间序列支持 NaN(非数值)值,允许您表示缺失或无效的测量值,同时保持数据的时序结构。
时间序列支持非常快速的读写操作,使其非常适合以下应用场景:
- 仪器数据记录
- 系统性能指标
- 金融行情数据
- 物联网(IoT)传感器数据
- 智能计量
- 服务质量(QoS)监控
Redis 时间序列在 Redis 开源版、Redis Software 和 Redis Cloud 中均可使用。 有关完整安装说明,请参阅 安装 Redis 开源版 或 安装 Redis Software。
创建时间序列
您可以使用 TS.CREATE 命令创建一个新的空时间序列,并指定键名。或者,如果您使用 TS.ADD 向不存在的时间序列键添加数据,则会自动创建它(有关 TS.ADD 的更多信息,请参见下面的 添加数据点)。
难度: 初级
命令: TS.CREATE, TYPE, TS.INFO
复杂度:
- TS.CREATE: O(1)
- TYPE: O(1)
- TS.INFO: O(1)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
String res1 = jedis.tsCreate("thermometer:1");
System.out.println(res1); // >>> OK
String res2 = jedis.type("thermometer:1");
System.out.println(res2); // >>> TSDB-TYPE
TSInfo res3 = jedis.tsInfo("thermometer:1");
System.out.println(res3.getProperty("totalSamples")); // >>> 0每个数据点的时间戳是一个 64 位整数值。该值表示 Unix 时间戳,以毫秒为单位,自 Unix 纪元 起计算。 创建时间序列时,您可以指定数据的最大保留期限(相对于最后报告的时间戳)。保留期限为 0 表示数据永不过期。
难度: 中级
基于: create
命令: TS.ADD, TS.INFO
复杂度:
- TS.ADD: O(M)
- TS.INFO: O(1)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
long res4 = jedis.tsAdd("thermometer:2", 1L, 10.8,
TSCreateParams.createParams().retention(100));
System.out.println(res4); // >>> 1
TSInfo res5 = jedis.tsInfo("thermometer:2");
System.out.println(res5.getProperty("retentionTime")); // >>> 100您还可以在创建时间序列时为其添加一个或多个 标签。标签是名称-值对,其中名称和值均为字符串。您可以使用这些名称和值来为查询和聚合选择所有可用时间序列的子集。
难度: 初级
基于: create
命令: TS.ADD, TS.INFO
复杂度:
- TS.ADD: O(M)
- TS.INFO: O(1)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
Map<String, String> labels = new HashMap<>();
labels.put("location", "UK");
labels.put("type", "Mercury");
long res6 = jedis.tsAdd("thermometer:3", 1L, 10.4,
TSCreateParams.createParams().labels(labels));
System.out.println(res6); // >>> 1
TSInfo res7 = jedis.tsInfo("thermometer:3");
System.out.println("Labels: " + res7.getLabels());
// >>> Labels: {location=UK, type=Mercury}添加数据点
您可以使用 TS.ADD 添加单个数据点,也可以使用 TS.MADD 在单个命令中向一个或多个时间序列添加多个数据点。(注意,与 TS.ADD 不同,TS.MADD 不会在您指定不存在的键时创建新的时间序列。)返回值是一个数组,包含操作后每个时间序列中的样本数量。如果您使用 * 字符作为时间戳,Redis 将记录服务器时钟报告的当前 Unix 时间。
难度: 初级
基于: create
命令: TS.MADD
复杂度:
- TS.MADD: O(N*M)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
List<Long> res8 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("thermometer:1", new TSElement(1L, 9.2)),
new AbstractMap.SimpleEntry<>("thermometer:1", new TSElement(2L, 9.9)),
new AbstractMap.SimpleEntry<>("thermometer:2", new TSElement(2L, 10.3))
);
System.out.println(res8); // >>> [1, 2, 2]查询数据点
使用 TS.GET 检索时间序列中具有最高时间戳的数据点。这会返回时间戳和值。
难度: 初级
基于: madd
命令: TS.GET
复杂度:
- TS.GET: O(1)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
// The last recorded temperature for thermometer:2
// was 10.3 at time 2.
TSElement res9 = jedis.tsGet("thermometer:2");
System.out.println("(" + res9.getTimestamp() + ", " + res9.getValue() + ")");
// >>> (2, 10.3)使用 TS.RANGE 检索时间序列中落入给定时间戳范围内的数据点。该范围是包含性的,即时间戳等于范围起始或结束的样本也会被包含在内。您可以使用 - 和 + 分别表示范围的最小和最大时间戳。响应是一个按时间戳升序返回的时间戳-值对数组。如果您希望结果按降序排列,请使用相同参数的 TS.REVRANGE。
难度: 中级
基于: madd
命令: TS.CREATE, TS.MADD, TS.RANGE, TS.REVRANGE
复杂度:
- TS.CREATE: O(1)
- TS.MADD: O(N*M)
- TS.RANGE: O(n/m+k)
- TS.REVRANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
// Add 5 data points to a time series named "rg:1"
String res10 = jedis.tsCreate("rg:1");
System.out.println(res10); // >>> OK
List<Long> res11 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:1", new TSElement(0L, 18.0)),
new AbstractMap.SimpleEntry<>("rg:1", new TSElement(1L, 14.0)),
new AbstractMap.SimpleEntry<>("rg:1", new TSElement(2L, 22.0)),
new AbstractMap.SimpleEntry<>("rg:1", new TSElement(3L, 18.0)),
new AbstractMap.SimpleEntry<>("rg:1", new TSElement(4L, 24.0))
);
System.out.println(res11); // >>> [0, 1, 2, 3, 4]
// Retrieve all the data points in ascending order
List<TSElement> res12 = jedis.tsRange("rg:1", 0L, 4L);
System.out.println(res12);
// >>> [(0:18.0), (1:14.0), (2:22.0), (3:18.0), (4:24.0)]
// Retrieve data points up to time 1 (inclusive)
List<TSElement> res13 = jedis.tsRange("rg:1", 0L, 1L);
System.out.println(res13);
// >>> [(0:18.0), (1:14.0)]
// Retrieve data points from time 3 onwards
List<TSElement> res14 = jedis.tsRange("rg:1", 3L, 4L);
System.out.println(res14);
// >>> [(3:18.0), (4:24.0)]
// Retrieve all the data points in descending order
List<TSElement> res15 = jedis.tsRevRange("rg:1", 0L, 4L);
System.out.println(res15);
// >>> [(4:24.0), (3:18.0), (2:22.0), (1:14.0), (0:18.0)]
// Retrieve data points up to time 1 (inclusive), in descending order
List<TSElement> res16 = jedis.tsRevRange("rg:1", 0L, 1L);
System.out.println(res16);
// >>> [(1:14.0), (0:18.0)]TS.RANGE 和 TS.REVRANGE 都允许您过滤结果。指定时间戳列表以仅包含具有这些精确时间戳的样本(如果使用此选项,您仍然必须指定时间戳范围参数)。指定最小值和最大值以仅包含该范围内的样本。值范围是包含性的,您可以使用相同的最小值和最大值来过滤单个值。
难度: 中级
基于: range
命令: TS.RANGE, TS.REVRANGE
复杂度:
- TS.RANGE: O(n/m+k)
- TS.REVRANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
List<TSElement> res17 = jedis.tsRange("rg:1",
TSRangeParams.rangeParams()
.fromTimestamp(0L)
.toTimestamp(4L)
.filterByTS(0L, 2L, 4L)
);
System.out.println(res17);
// >>> [(0:18.0), (2:22.0), (4:24.0)]
List<TSElement> res18 = jedis.tsRevRange("rg:1",
TSRangeParams.rangeParams()
.fromTimestamp(0L)
.toTimestamp(4L)
.filterByTS(0L, 2L, 4L)
.filterByValues(20.0, 25.0)
);
System.out.println(res18);
// >>> [(4:24.0), (2:22.0)]
List<TSElement> res19 = jedis.tsRevRange("rg:1",
TSRangeParams.rangeParams()
.fromTimestamp(0L)
.toTimestamp(4L)
.filterByTS(0L, 2L, 4L)
.filterByValues(22.0, 22.0)
.count(1)
);
System.out.println(res19);
// >>> [(2:22.0)]查询多个时间序列
TS.GET、TS.RANGE 和 TS.REVRANGE 命令也有对应的 TS.MGET、 TS.MRANGE 和 TS.MREVRANGE 版本,用于操作多个时间序列。TS.MGET 从每个时间序列返回具有最高时间戳的数据点,而 TS.MRANGE 和 TS.MREVRANGE 返回每个时间序列中时间戳范围内的数据点。
参数基本相同,除了多时间序列命令不以键名作为第一个参数。相反,您需要指定一个过滤器表达式,以仅包含具有特定标签的时间序列。(请参阅上面的 创建时间序列 了解如何为时间序列添加标签。)过滤器表达式使用简单的语法,允许您根据标签的存在或值来包含或排除时间序列。有关过滤器语法的详细信息,请参阅 TS.MGET 命令参考中的描述。您还可以请求数据点返回所有标签或选定的标签子集。
难度: 高级
基于: create_labels
命令: TS.CREATE, TS.MADD, TS.MGET, TS.MRANGE, TS.MREVRANGE
复杂度:
- TS.CREATE: O(1)
- TS.MADD: O(N*M)
- TS.MGET: O(n)
- TS.MRANGE: O(n/m+k)
- TS.MREVRANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
// Create three new "rg:" time series (two in the US
// and one in the UK, with different units) and add some
// data points.
Map<String, String> usLabels1 = new HashMap<>();
usLabels1.put("location", "us");
usLabels1.put("unit", "cm");
Map<String, String> usLabels2 = new HashMap<>();
usLabels2.put("location", "us");
usLabels2.put("unit", "in");
Map<String, String> ukLabels = new HashMap<>();
ukLabels.put("location", "uk");
ukLabels.put("unit", "mm");
String res20 = jedis.tsCreate("rg:2",
TSCreateParams.createParams().labels(usLabels1));
System.out.println(res20); // >>> OK
String res21 = jedis.tsCreate("rg:3",
TSCreateParams.createParams().labels(usLabels2));
System.out.println(res21); // >>> OK
String res22 = jedis.tsCreate("rg:4",
TSCreateParams.createParams().labels(ukLabels));
System.out.println(res22); // >>> OK
List<Long> res23 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:2", new TSElement(0L, 1.8)),
new AbstractMap.SimpleEntry<>("rg:3", new TSElement(0L, 0.9)),
new AbstractMap.SimpleEntry<>("rg:4", new TSElement(0L, 25.0))
);
System.out.println(res23); // >>> [0, 0, 0]
List<Long> res24 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:2", new TSElement(1L, 2.1)),
new AbstractMap.SimpleEntry<>("rg:3", new TSElement(1L, 0.77)),
new AbstractMap.SimpleEntry<>("rg:4", new TSElement(1L, 18.0))
);
System.out.println(res24); // >>> [1, 1, 1]
List<Long> res25 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:2", new TSElement(2L, 2.3)),
new AbstractMap.SimpleEntry<>("rg:3", new TSElement(2L, 1.1)),
new AbstractMap.SimpleEntry<>("rg:4", new TSElement(2L, 21.0))
);
System.out.println(res25); // >>> [2, 2, 2]
List<Long> res26 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:2", new TSElement(3L, 1.9)),
new AbstractMap.SimpleEntry<>("rg:3", new TSElement(3L, 0.81)),
new AbstractMap.SimpleEntry<>("rg:4", new TSElement(3L, 19.0))
);
System.out.println(res26); // >>> [3, 3, 3]
List<Long> res27 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("rg:2", new TSElement(4L, 1.78)),
new AbstractMap.SimpleEntry<>("rg:3", new TSElement(4L, 0.74)),
new AbstractMap.SimpleEntry<>("rg:4", new TSElement(4L, 23.0))
);
System.out.println(res27); // >>> [4, 4, 4]
// Retrieve the last data point from each US time series.
Map<String, TSMGetElement> res28 = jedis.tsMGet(
TSMGetParams.multiGetParams().latest(),
"location=us"
);
System.out.println(res28);
// >>> {rg:2=TSMGetElement{key=rg:2, labels={}, element=(4:1.78)}...
// Retrieve the same data points, but include the `unit`
// label in the results.
Map<String, TSMGetElement> res29 = jedis.tsMGet(
TSMGetParams.multiGetParams().selectedLabels("unit"),
"location=us"
);
System.out.println(res29);
// >>> {rg:2=TSMGetElement{key=rg:2, labels={unit=cm}, element=(4:1.78)}...
// Retrieve data points up to time 2 (inclusive) from all
// time series that use millimeters as the unit. Include all
// labels in the results.
Map<String, TSMRangeElements> res30 = jedis.tsMRange(
TSMRangeParams.multiRangeParams(0L, 2L)
.withLabels()
.filter("unit=mm")
);
System.out.println(res30);
// >>> {rg:4=TSMRangeElements{key=rg:4, labels={location=uk, unit=mm}, value=[(0:25.0), (1:18.0), (2:21.0)]}}
// Retrieve data points from time 1 to time 3 (inclusive) from
// all time series that use centimeters or millimeters as the unit,
// but only return the `location` label. Return the results
// in descending order of timestamp.
Map<String, TSMRangeElements> res31 = jedis.tsMRevRange(
TSMRangeParams.multiRangeParams(1L, 3L)
.selectedLabels("location")
.filter("unit=(cm,mm)")
);
System.out.println(res31);
// >>> {rg:2=TSMRangeElements{key=rg:2, labels={location=us, unit=cm}, value=[(1:2.1)...聚合
如果样本添加非常频繁,时间序列可能会变得很大。与其处理单个样本,有时将整个时间范围划分为等大小的“桶”,并用聚合值(如平均值或最大值)表示每个桶会更有用。
例如,如果您预计一天内收集超过十亿个数据点,您可以使用一分钟的桶来聚合数据。由于每个桶用一个值表示,这将数据集大小减少到 1,440 个数据点(24 小时 x 60 分钟 = 1,440 分钟)。
范围查询命令允许您指定一个或多个聚合器和桶大小。 可用的聚合器有:
| 聚合器 | 描述 |
|---|---|
avg | 所有非 NaN 值的算术平均值 |
sum | 所有非 NaN 值的和 |
min | 非 NaN 值的最小值 |
max | 非 NaN 值的最大值 |
range | 最大值与最小值之差(针对非 NaN 值) |
count | 非 NaN 值的数量 |
countNaN | NaN 值的数量(自 Redis 8.6 起) |
countAll | 所有值的数量,包括 NaN 和非 NaN(自 Redis 8.6 起) |
first | 桶中时间戳最小的非 NaN 值 |
last | 桶中时间戳最大的非 NaN 值 |
std.p | 非 NaN 值的总体标准差 |
std.s | 非 NaN 值的样本标准差 |
var.p | 非 NaN 值的总体方差 |
var.s | 非 NaN 值的样本方差 |
twa | 桶时间范围内的加权平均值(忽略 NaN 值)(自 RedisTimeSeries 1.8) |
例如,下面的示例展示了在 rg:2 时间序列的所有五个数据点上使用 avg 聚合器进行聚合。桶大小为 2 毫秒,因此有三个聚合值,最后一个桶只有一个值用于计算平均值。
难度: 中级
基于: madd
命令: TS.RANGE
复杂度:
- TS.RANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
List<TSElement> res32 = jedis.tsRange("rg:2",
TSRangeParams.rangeParams()
.fromTimestamp(0L)
.toTimestamp(4L)
.aggregation(AggregationType.AVG, 2)
);
System.out.println(res32);
// >>> [(0:1.9500000000000002), (2:2.0999999999999996), (4:1.78)]要在单个查询中使用多个聚合器,请用逗号分隔聚合器,如下例所示。
TS.RANGE rg:2 - + AGGREGATION min,avg,max 2桶对齐
桶序列有一个参考时间戳,即序列中第一个桶的起始时间戳。默认情况下,参考时间戳为零。 例如,以下命令创建一个时间序列,并在默认零对齐下应用 min 聚合器,桶大小为 25 毫秒。
难度: 中级
基于: agg
命令: TS.CREATE, TS.MADD, TS.RANGE
复杂度:
- TS.CREATE: O(1)
- TS.MADD: O(N*M)
- TS.RANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
String res33 = jedis.tsCreate("sensor3");
System.out.println(res33); // >>> OK
List<Long> res34 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(10L, 1000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(20L, 2000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(30L, 3000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(40L, 4000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(50L, 5000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(60L, 6000.0)),
new AbstractMap.SimpleEntry<>("sensor3", new TSElement(70L, 7000.0))
);
System.out.println(res34); // >>> [10, 20, 30, 40, 50, 60, 70]
List<TSElement> res35 = jedis.tsRange("sensor3",
TSRangeParams.rangeParams()
.fromTimestamp(10L)
.toTimestamp(70L)
.aggregation(AggregationType.MIN, 25)
);
System.out.println(res35);
// >>> [(0:1000.0), (25:3000.0), (50:5000.0)]下图展示了聚合桶及其与零时间参考时间戳的对齐方式。
Value: | (1000) (2000) (3000) (4000) (5000) (6000) (7000)
Timestamp: |-------|10|-------|20|-------|30|-------|40|-------|50|-------|60|-------|70|--->
Bucket(25ms): |_________________________||_________________________||___________________________|
V V V
min(1000, 2000)=1000 min(3000, 4000)=3000 min(5000, 6000, 7000)=5000您也可以将桶对齐到查询范围的起始或结束。例如,以下命令将桶对齐到查询范围的起始时间 10。
难度: 高级
基于: agg_bucket
命令: TS.RANGE
复杂度:
- TS.RANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
List<TSElement> res36 = jedis.tsRange("sensor3",
TSRangeParams.rangeParams()
.fromTimestamp(10L)
.toTimestamp(70L)
.aggregation(AggregationType.MIN, 25)
.alignStart()
);
System.out.println(res36);
// >>> [(10:1000.0), (35:4000.0), (60:6000.0)]下图展示了这种桶排列方式。
Value: | (1000) (2000) (3000) (4000) (5000) (6000) (7000)
Timestamp: |-------|10|-------|20|-------|30|-------|40|-------|50|-------|60|-------|70|--->
Bucket(25ms): |__________________________||_________________________||___________________________|
V V V
min(1000, 2000, 3000)=1000 min(4000, 5000)=4000 min(6000, 7000)=6000跨时间序列聚合
默认情况下,TS.MRANGE 和 TS.MREVRANGE 的结果按时间序列分组。但是,您可以使用 GROUPBY 和 REDUCE 选项按标签分组,并对具有相同时间戳和相同标签值的元素应用聚合(此功能自 RedisTimeSeries v1.6 起可用)。
例如,以下命令创建四个时间序列,两个用于英国,两个用于美国,并添加一些数据点。第一个 TS.MRANGE 命令按国家分组结果,并应用 max 聚合以查找每个国家在每个时间戳的最大样本值。第二个 TS.MRANGE 命令使用相同的分组,但应用 avg 聚合。
难度: 高级
基于: agg, create_labels
命令: TS.CREATE, TS.MADD, TS.MRANGE
复杂度:
- TS.CREATE: O(1)
- TS.MADD: O(N*M)
- TS.MRANGE: O(n/m+k)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
Map<String, String> ukCountry = new HashMap<>();
ukCountry.put("country", "uk");
Map<String, String> usCountry = new HashMap<>();
usCountry.put("country", "us");
jedis.tsCreate("wind:1", TSCreateParams.createParams().labels(ukCountry));
jedis.tsCreate("wind:2", TSCreateParams.createParams().labels(ukCountry));
jedis.tsCreate("wind:3", TSCreateParams.createParams().labels(usCountry));
jedis.tsCreate("wind:4", TSCreateParams.createParams().labels(usCountry));
jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("wind:1", new TSElement(0L, 10.0)),
new AbstractMap.SimpleEntry<>("wind:2", new TSElement(0L, 12.0)),
new AbstractMap.SimpleEntry<>("wind:3", new TSElement(0L, 8.0)),
new AbstractMap.SimpleEntry<>("wind:4", new TSElement(0L, 15.0))
);
jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("wind:1", new TSElement(1L, 11.0)),
new AbstractMap.SimpleEntry<>("wind:2", new TSElement(1L, 13.0)),
new AbstractMap.SimpleEntry<>("wind:3", new TSElement(1L, 9.0)),
new AbstractMap.SimpleEntry<>("wind:4", new TSElement(1L, 16.0))
);
jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("wind:1", new TSElement(2L, 9.0)),
new AbstractMap.SimpleEntry<>("wind:2", new TSElement(2L, 11.0)),
new AbstractMap.SimpleEntry<>("wind:3", new TSElement(2L, 7.0)),
new AbstractMap.SimpleEntry<>("wind:4", new TSElement(2L, 14.0))
);
jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("wind:1", new TSElement(3L, 12.0)),
new AbstractMap.SimpleEntry<>("wind:2", new TSElement(3L, 14.0)),
new AbstractMap.SimpleEntry<>("wind:3", new TSElement(3L, 10.0)),
new AbstractMap.SimpleEntry<>("wind:4", new TSElement(3L, 17.0))
);
jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("wind:1", new TSElement(4L, 8.0)),
new AbstractMap.SimpleEntry<>("wind:2", new TSElement(4L, 10.0)),
new AbstractMap.SimpleEntry<>("wind:3", new TSElement(4L, 6.0)),
new AbstractMap.SimpleEntry<>("wind:4", new TSElement(4L, 13.0))
);
// Group by country with max reduction
Map<String, TSMRangeElements> res44 = jedis.tsMRange(
TSMRangeParams.multiRangeParams(0L, 4L)
.filter("country=(us,uk)")
.groupBy("country", "max"));
System.out.println(res44);
// >>> {country=uk=TSMRangeElements{key=country=uk, labels={}, value=[(0:12.0)...
// Group by country with avg reduction
Map<String, TSMRangeElements> res45 = jedis.tsMRange(
TSMRangeParams.multiRangeParams(0L, 4L)
.filter("country=(us,uk)")
.groupBy("country", "avg"));
System.out.println(res45);
// >>> {country=uk=TSMRangeElements{key=country=uk, labels={}, value=[(0:11.0)...NaN 值
从 Redis 8.6 开始,时间序列支持 NaN(非数值)值。此功能允许您插入表示缺失、无效或未知数据的测量值,同时保留时间序列的时间结构。
NaN 值的用例
NaN 值在需要区分以下情况的场景中很有用:
- 缺失数据:未进行测量的时间戳
- 无效数据:尝试测量但失败的时间戳(例如,传感器故障)
- 未知数据:测量值在语义上“未知”且可能稍后填充的时间戳
NaN 行为
- 添加 NaN 值:使用
TS.ADD和TS.MADD插入 NaN 值 - 查询 NaN 值:所有原始测量查询(
TS.GET、TS.RANGE等)在结果中包含 NaN 值 - NaN 聚合:除
countNaN和countAll外,所有现有聚合器都忽略 NaN 值。使用countNaN和countAll分别计算 NaN 值和总数值 - 递增/递减:当当前值或操作数为 NaN 时,
TS.INCRBY和TS.DECRBY返回错误 - 重复策略:在混合 NaN 和非 NaN 值时,
MIN、MAX和SUM策略有特殊处理 - 过滤:
FILTER_BY_VALUE参数不能为 NaN 值 - 忽略重复:使用
IGNORE参数时,NaN 值永远不会被视为重复
# Create a temperature sensor time series
> TS.CREATE sensor:temp LABELS type temperature location outdoor
# Add normal readings and NaN for sensor malfunction
> TS.MADD sensor:temp 1000 25.5 sensor:temp 1010 26.1 sensor:temp 1020 NaN sensor:temp 1030 24.8
1) (integer) 1000
2) (integer) 1010
3) (integer) 1020
4) (integer) 1030
# Query all values - NaN is included in raw results
> TS.RANGE sensor:temp - +
1) 1) (integer) 1000
2) 25.5
2) 1) (integer) 1010
2) 26.1
3) 1) (integer) 1020
2) NaN
4) 1) (integer) 1030
2) 24.8
# Aggregate ignores NaN - average of 25.5, 26.1, 24.8 = 25.47
> TS.RANGE sensor:temp - + AGGREGATION avg 1000
1) 1) (integer) 1000
2) 25.466666666666665
# Count non-NaN values
> TS.RANGE sensor:temp - + AGGREGATION count 1000
1) 1) (integer) 1000
2) 3
# Count NaN values
> TS.RANGE sensor:temp - + AGGREGATION countNaN 1000
1) 1) (integer) 1000
2) 1
# Count all values
> TS.RANGE sensor:temp - + AGGREGATION countAll 1000
1) 1) (integer) 1000
2) 4压缩
聚合 查询允许您从大数据集中提取重要信息,形成更小、更易管理的数据集。如果您持续向时间序列中添加新生成的数据,您可能需要定期对最新数据运行相同的聚合。您无需每次手动运行查询,而是可以向时间序列添加 压缩规则,以便在数据到达时增量计算聚合。聚合桶的值存储在一个单独的时间序列中,原始序列保持不变。
使用 TS.CREATERULE 创建压缩规则,指定源和目标时间序列键、聚合器和桶持续时间。请注意,创建规则时目标时间序列必须已存在,并且压缩仅处理在创建规则后添加到源序列中的数据。
例如,您可以使用以下命令创建一个时间序列以及一个压缩规则,以查找每 3 毫秒周期内的最小值。
难度: 高级
基于: create
命令: TS.CREATE, TS.CREATERULE, TS.INFO
复杂度:
- TS.CREATE: O(1)
- TS.CREATERULE: O(1)
- TS.INFO: O(1)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
String res46 = jedis.tsCreate("hyg:1");
System.out.println(res46); // >>> OK
String res47 = jedis.tsCreate("hyg:compacted");
System.out.println(res47); // >>> OK
String res48 = jedis.tsCreateRule("hyg:1", "hyg:compacted", AggregationType.MIN, 3);
System.out.println(res48); // >>> OK
TSInfo res49 = jedis.tsInfo("hyg:1");
System.out.println("Rules: " + res49.getProperty("rules"));
// >>> Rules: [{compactionKey=hyg:compacted, bucketDuration=3, aggregationType=MIN, alignmentTimestamp=0}]
TSInfo res50 = jedis.tsInfo("hyg:compacted");
System.out.println("Source key: " + res50.getProperty("sourceKey"));
// >>> Source key: hyg:1在前 3 毫秒(第一个桶)内添加数据点不会在压缩序列中产生任何数据。但是,当您为时间 4(第二个桶)添加数据时,压缩规则会计算第一个桶的最小值并将其添加到压缩序列中。
难度: 中级
基于: create_compaction
命令: TS.MADD, TS.RANGE, TS.ADD
复杂度:
- TS.MADD: O(N*M)
- TS.RANGE: O(n/m+k)
- TS.ADD: O(M)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
List<Long> res51 = jedis.tsMAdd(
new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(0L, 75.0)),
new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(1L, 77.0)),
new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(2L, 78.0))
);
System.out.println(res51); // >>> [0, 1, 2]
List<TSElement> res52 = jedis.tsRange("hyg:compacted", 0L, 10L);
System.out.println(res52); // >>> []
long res53 = jedis.tsAdd("hyg:1", 3L, 79.0);
System.out.println(res53); // >>> 3
List<TSElement> res54 = jedis.tsRange("hyg:compacted", 0L, 10L);
System.out.println(res54); // >>> [(0:75.0)]一般策略是,压缩规则不会为源序列中的最新桶添加数据到压缩序列,但会为任何先前的桶添加和更新压缩数据。这反映了在实时顺序添加数据样本的典型使用模式(聚合值在其桶周期结束之前通常不正确)。但请注意,当您向后续桶添加数据时,先前的桶不会被“关闭”。如果您在最新桶之前的桶中添加或 删除 数据,压缩规则仍会更新该桶的压缩数据。
删除数据点
使用 TS.DEL 删除落入给定时间戳范围内的数据点。该范围是包含性的,即时间戳等于范围起始或结束的样本也会被删除。如果您想删除单个时间戳,请将其同时用作范围的起始和结束。
难度: 初级
基于: create
命令: TS.INFO, TS.ADD, TS.DEL
复杂度:
- TS.INFO: O(1)
- TS.ADD: O(M)
- TS.DEL: O(N)
可用客户端: Redis CLI, C#, Go, Java(同步 - Jedis), JavaScript(Node.js), Python
Java(同步 - Jedis)
TSInfo res55 = jedis.tsInfo("thermometer:1");
System.out.println(res55.getProperty("totalSamples")); // >>> 2
System.out.println(res55.getProperty("firstTimestamp")); // >>> 1
System.out.println(res55.getProperty("lastTimestamp")); // >>> 2
long res56 = jedis.tsAdd("thermometer:1", 3L, 9.7);
System.out.println(res56); // >>> 3
TSInfo res57 = jedis.tsInfo("thermometer:1");
System.out.println(res57.getProperty("totalSamples")); // >>> 3
long res58 = jedis.tsDel("thermometer:1", 1L, 2L);
System.out.println(res58); // >>> 2
TSInfo res59 = jedis.tsInfo("thermometer:1");
System.out.println(res59.getProperty("totalSamples")); // >>> 1
long res60 = jedis.tsDel("thermometer:1", 3L, 3L);
System.out.println(res60); // >>> 1
TSInfo res61 = jedis.tsInfo("thermometer:1");
System.out.println(res61.getProperty("totalSamples")); // >>> 0将时间序列与其他指标工具配合使用
在 RedisTimeSeries GitHub 组织中,您可以找到帮助您将 RedisTimeSeries 与其他工具集成的项目,包括:
- Prometheus,一个读写适配器,用于将 RedisTimeSeries 作为后端数据库。
- Grafana 7.1+,使用 Redis Data Source。
- Telegraf。从 InfluxData 下载插件。
- StatsD,Graphite 导出,使用 graphite 协议。
更多信息
本节中的其他页面更详细地描述了 RedisTimeSeries 的概念。 另请参阅 时间序列命令参考。