时间序列
您可以使用 Redis 堆栈在 Redis 企业版中管理时间序列数据。
特性
- 按开始时间和结束时间查询
- 按标签集查询
- 任何时间段的聚合查询(最小值、最大值、平均值、总和、范围、计数、第一个、最后一个)
- 可配置的最大保留时间
- 压缩/汇总 - 自动更新的聚合时间序列
- 标签索引 - 每个键都有标签,允许按标签查询
内存模型
时间序列是内存块的链接列表。每个块都有一个预定义的样本大小。每个样本都是一个时间和值的元组,包含 128 位,64 位用于时间戳,64 位用于值。
时间序列功能
Redis Stack 提供了一种新的数据类型,它使用固定大小的内存块来存储时间序列样本,并通过与 Redis 流相同的 Radix 树实现进行索引。使用流,您可以创建 一个限制流,有效地限制消息数量。对于时间序列,您可以以毫秒为单位应用保留策略。这更适合时间序列用例,因为它们通常只对给定时间窗口内的数据感兴趣,而不是对固定数量的样本感兴趣。
降采样/压缩
降采样前 | 降采样后 |
---|---|
![]() |
![]() |
如果您想无限期地保留所有原始数据点,那么您的数据集将随着时间的推移线性增长。但是,如果您的用例允许您在更早的时间拥有更不精细的数据,那么可以应用降采样。这允许您通过使用给定的聚合函数对给定时间窗口的原始数据进行聚合来保留更少的历史数据点。时间序列支持使用以下聚合进行 降采样:平均值、总和、最小值、最大值、范围、计数、第一个和最后一个。
辅助索引
使用 Redis 的核心数据结构时,您只能通过知道保存时间序列的确切键来检索时间序列。不幸的是,对于许多时间序列用例(例如根本原因分析或监控),您的应用程序将不知道它要查找的确切键。这些用例通常希望查询一组在几个维度上相互关联的时间序列,以提取您需要的洞察力。您可以使用 Redis 核心数据结构创建自己的辅助索引来帮助解决这个问题,但这将带来很高的开发成本,并且需要您管理边缘情况以确保索引正确。
Redis 基于名为 标签 的字段值
对为您执行此索引。您可以向每个时间序列添加标签,并在查询时使用它们进行 过滤。
以下是如何使用两个标签(sensor_id 和 area_id 分别为字段,值为 2 和 32)以及 60,000 毫秒的保留窗口创建时间序列的示例
TS.CREATE temperature RETENTION 60000 LABELS sensor_id 2 area_id 32
读取时的聚合
当您需要查询时间序列时,如果只对给定时间间隔内的平均值感兴趣,则流式传输所有原始数据点很麻烦。时间序列只传输确保最低延迟所需的最小数据量。
以下是如何在 5,000 毫秒的时间段内进行 聚合 的示例
127.0.0.1:12543> TS.RANGE temperature:3:32 1548149180000 1548149210000 AGGREGATION avg 5000
1) 1) (integer) 1548149180000
2) "26.199999999999999"
2) 1) (integer) 1548149185000
2) "27.399999999999999"
3) 1) (integer) 1548149190000
2) "24.800000000000001"
4) 1) (integer) 1548149195000
2) "23.199999999999999"
5) 1) (integer) 1548149200000
2) "25.199999999999999"
6) 1) (integer) 1548149205000
2) "28"
7) 1) (integer) 1548149210000
2) "20"
集成
Redis Stack 附带了对现有时间序列工具的几个集成。其中一个集成是我们的 RedisTimeSeries 适配器,用于 Prometheus,它将所有监控指标保留在时间序列中,同时利用整个 Prometheus 生态系统。

此外,我们还为 Grafana 创建了直接集成。 此仓库 包含 RedisTimeSeries、其远程写入适配器、Prometheus 和 Grafana 的 docker-compose 设置。它还带有一组数据生成器和预构建的 Grafana 仪表盘。
使用 Redis 的时间序列建模方法
数据建模方法
Redis 流允许您在给定时间戳的消息中添加几个字段值对。对于每个设备,我们收集了 10 个指标,这些指标被建模为单个流消息中的 10 个单独的字段。

对于有序集合,我们以两种不同的方式对数据进行了建模。对于“每个设备的有序集合”,我们将指标连接起来并用冒号隔开,例如“<timestamp>:<metric1>:<metric2>: … :<metric10>”
。

当然,这会消耗更少的内存,但需要更多的 CPU 周期才能在读取时获得正确的指标。这也意味着更改每个设备的指标数量并不容易,这就是我们还对第二个有序集合方法进行基准测试的原因。在“每个指标的有序集合”中,我们将每个指标保存在自己的有序集合中,每个设备有 10 个有序集合。我们将以“<timestamp>:<metric>”
格式记录值。

另一种替代方法是通过创建具有唯一键的哈希来规范化数据,以跟踪给定时间戳下给定设备的所有测量值。然后,此键将成为有序集合中的值。但是,为了读取时间序列而必须访问许多哈希,这会在读取时带来巨大的成本,因此我们放弃了这条路。
每个时间序列都保存一个指标。我们选择这种设计是为了保持 Redis 的原则,即大量的小键比少量的大键更好。

我们的基准测试没有利用时间序列开箱即用的辅助索引功能。Redis 在每个分片中保留一个部分辅助索引,并且由于索引继承了其索引的键的相同哈希槽,因此它始终托管在同一个分片上。这种方法将使本机数据结构的设置更加复杂,因此为了简单起见,我们决定不将其包含在我们的基准测试中。此外,虽然 Redis Enterprise 可以使用 代理 将诸如 TS.MGET 和 TS.MRANGE 之类的命令的请求扇出到所有分片并聚合结果,但我们也选择不在基准测试中利用此优势。
数据摄取
在基准测试的数据摄取部分,我们通过衡量每秒可以摄取多少个设备的数据来比较这四种方法。我们的客户端有 8 个工作线程,每个线程有 50 个连接,每个请求有 50 个命令的管道。
每种方法的摄取细节
Redis 流 | 时间序列 | 有序集合 每个设备 |
有序集合 每个指标 |
|
---|---|---|---|---|
命令 | XADD | TS.MADD | ZADD | ZADD |
管道 | 50 | 50 | 50 | 50 |
每个请求的指标 | 5000 | 5000 | 5000 | 500 |
键的数量 | 4000 | 40000 | 4000 | 40000 |

我们所有的摄取操作都在毫秒级延迟下执行。尽管两者都使用相同的数据结构,但时间序列方法的吞吐量略高于 Redis 流。
每种方法都会产生不同的结果,这表明了针对特定用例进行原型设计的价值。正如我们在查询性能中看到的那样,每个设备的有序集合会带来更高的写入吞吐量,但以牺牲查询性能为代价。这是摄取、查询性能和灵活性之间的权衡(请记住之前的数据建模说明)。
读取性能
我们在本基准测试中使用的读取查询查询单个时间序列,并在一个小时的时间段内将其聚合,方法是保留每个段中观察到的最大 CPU 百分比。我们查询中考虑的时间范围正好是一个小时,因此只返回一个最大值。对于时间序列,这是开箱即用的功能。
127.0.0.1:12543> TS.RANGE cpu_usage_user{1340993056} 1451606390000 1451609990000 AGGREGATION max 3600000
对于 Redis 流和有序集合方法,我们创建了 以下 LUA 脚本。客户端再次拥有 8 个线程,每个线程有 50 个连接。由于我们执行的是相同的查询,因此只有一个分片被命中,并且在这四种情况下,此分片都达到了 100% 的 CPU 利用率。

这就是您能够看到拥有针对给定用例的专用数据结构以及与之一起运行的工具箱的真正力量所在。使用时间序列优于所有其他方法,并且是唯一能够实现毫秒级响应时间的方案。
内存利用率
对于 Redis 流和有序集合方法,样本都被存储为字符串,而时间序列将它们存储为双精度数。在这个特定的数据集中,我们选择了一个 CPU 测量值,其四舍五入的整数值介于 0-100 之间,因此作为字符串会消耗 2 个字节的内存。使用时间序列,每个指标都具有 64 位精度。
与两种有序集合方法相比,时间序列可以显着降低内存消耗。鉴于时间序列数据的无限性,这通常是评估的关键标准 - 需要在内存中保留的整体数据集大小。Redis 流进一步降低了内存消耗,但在需要更多数字以获得更高精度时,它会与时间序列相同或更高。
