(注:这篇博文改编自我于 6 月份进行的一次网络研讨会。要更深入地了解 RedisTimeSeries,请立即注册并观看网络研讨会!)
大多数开发者都知道 Redis 的实时响应能力使其非常适合处理时间序列数据。但时间序列数据究竟是什么呢?许多定义会写满一页又一页的解释,但我认为可以将其大大简化:
基本上,时间序列数据是将时间编码为索引,并且每个记录的时间都有一个数值的数据。如果将其可视化为两列,一列将包含某种时间索引,通常是 Unix 纪元格式的时间戳。另一列将包含某种数值。
非常简单。
关键在于,您可以使用时间范围来分析时间序列数据,例如,查看 1 月 1 日到 1 月 3 日之间发生了什么。您还可以精确到秒,有时甚至毫秒。您还可以将数据按时间单位分开,以查看每小时发生了什么。然后,如果您不想查看时间序列数据中的每一个事件,可以在其之上进行聚合,例如获取每小时的平均值。
许多人在想到时间序列数据时会想象股票图表。这是查看股票在给定时间段内的表现的好方法。我经常关注的一个时间序列数据用例是服务器在任何指定间隔内的 CPU 负载。时间序列数据也是查看传感器数据和其他物联网 (IoT) 信息的良好方式。任何时候您查看随时间变化的趋势时,这些数据通常都源自某种时间序列数据库或时间序列结构。
现在让我们重点关注 Redis 和时间序列。这一切都始于 Sorted Sets,它是 Redis 内置的数据结构之一。人们很早就开始使用 Sorted Sets 来处理时间序列数据,看起来像这样
> ZADD mySortedSet 1559938522 1000
这个例子包含了 ZADD 命令,mySortedSets 作为键,以及一个时间戳作为分数。最后是成员,也就是值。
这很不错,但你只能获取范围数据,无法进行平均或降采样。
集合不能有重复项。在这里,如果您有两个不同的时间戳但值相同,则集合是基于成员的(在本例中,我们将其定义为值)。因此在下面的示例中,第二个实际上会是一个 upsert——它会覆盖第一个。这不适用于时间序列数据,人们以这种方式使用时会遇到一些棘手的问题:
> ZADD mySortedSet 1559938522 1000
> ZADD mySortedSet 1559938534 1000
开发者想出了许多计算复杂且非常难以实现的变通方法。肯定有更简单的方法。
大约两年前,Redis 4.0 发布,带来了 Redis Streams,它旨在解决基于统一日志架构构建应用程序和进行进程间消息传递的问题。
与 Sorted Sets 相比,Redis Streams 在时间序列用例方面提供了重要的优势。它支持自动生成的 ID、无重复项以及每个样本的字段/值对。
> XADD myStream * myValue 1000
> XADD myStream * myValue 1000 anotherField hello
如第一个命令所示,我们将字段 myField 设置为 1000。在第二个命令中,创建了一个新条目,其中 myValue 设置为 1000,同时 anotherField 设置为 hello。这些都是位于键 myStream 的流中的条目。
但这仍然缺乏重要的功能,并且并非真正为时间序列数据设计。你可以轻松获取时间范围,但其他功能不多。
现在让我们稍微回顾一下,谈谈 Redis 模块 API,它在 Streams 之前不久发布,允许 Redis 拥有更多的社区和数据类型。Redis 用户可以构建在 Redis 内部充当“一等公民”的模块。现有的模块包括从 RediSearch 到 RedisGraph 再到 RedisJSON 的所有模块。现在还有 RedisTimeSeries,它基本上在 Redis 内部创建了一个完整的时间序列数据库。
在我们了解如何使用 RedisTimeSeries 模块之前,了解其底层原理非常重要。
您首先需要了解的是“数据块”(chunk)。您实际上永远不会直接操作数据块,但 RedisTimeSeries 将所有数据存储在这些数据块中。每个数据块由双向链表中的两个相关联的数组组成(一个用于时间戳,一个用于样本值)。
例如,假设我想将一个时间戳放入我的时间序列数据库。它会进入两个数组的第一行。如果您有额外的样本,它们也会进入数组。
数据块是固定大小的。当数据块满时,额外的数据会自动进入下一个数据块。向链表开头或结尾添加计算量微不足道,因此添加新数据块时非常轻量级。
但与大多数 Redis 数据类型不同的是,最佳实践是先创建您的时间序列键。在本例中,我的命令是 TS.CREATE。然后我有了 myTS,这就是我在这里使用的键。
所以假设我们想向这个键添加一些元数据。想象我们正在经营一个蔬菜苗圃,我们想追踪 4 号温室里的第 47 棵卷心菜;我们将这些元数据称为标签。这将应用于整个时间序列中的每一个样本
处理时间序列数据的另一个重要部分是保留策略。假设我们不关心任何超过 60 秒的数据。RedisTimeSeries 可以删除超出您指定保留时间范围的数据。
我们可以使用一个称为 TS.ADD 的操作来添加值。第一个参数是键 myTS,星号语法借鉴自 Redis Streams,表示 Redis 将自动生成时间戳。在这种情况下,值为 834。
接着,我们添加另一个样本,并指定一个时间戳。请注意,时间戳实际上是只能追加的,所以您不能在最近使用的时间戳之前插入数据。后续的 TS.ADD 必须使用大于该值的时间戳。
接下来,为了获取有界结果,您可以查询两个时间戳之间的所有样本。根据我们的例子,您可以看到第一个时间戳的值是 834,第二个是 1000。
这很有用,但也许您想获取每 30 秒时间段的平均值。在这里,avg 是我们的关键字,当然,917 是 834 和 1,000 的平均值。
但是当您拥有更多数据时会发生什么?您可能不想一直运行 TS.RANGE 命令,而只想细粒度地提取数据。
好,我们有能力创建规则!myTS 是我的键:这是源。目标是 myTS2,这是第二个键。这里的所有数据块都代表 30 秒的时间,RedisTimeSeries 会自动将其放入目标键的辅助键中。所以每过 30 秒,myTS2 就会增加一个样本。
等等,还有更多!它不仅仅局限于平均值。您可以求和、求最小值、求最大值、求范围。您可以获取计数——有多少个——以及第一个或最后一个。所有这些不同的聚合函数也适用于 TS RANGE。
让我们看看 RedisTimeSeries 还能做什么。命令 TS.INCRBY 和 TS.DECRBY 用于随时间计数。TS.INCRBY 将前一个条目增加某个值。假设您知道在 10 秒内收集了 10 个小部件。您就可以在一个键上运行 TS.INCRBY。这样您就不必知道前一个值,并且可以保持一个运行中的计数。TS.DECRBY 的原理相同,只是方向相反。
TS.GET 同时获取最后一个值。而 TS.ALTER 允许您更改已创建键的元数据,包括字段、值保留策略等。
TS.MRANGE 和 TS.MGET 很有趣,但解释起来有点复杂。RedisTimeSeries 跟踪数据库中所有不同的时间序列键。TS.MRANGE 允许您按标签的键/值对进行指定。因此,在我们的温室示例中,您可以获取 4 号温室的温度读数,然后使用 TS.MRANGE 查看整个键空间中的不同键。类似地,TS.MGET 允许您按标签获取单个最新值。您可以将 RedisTimeSeries 连接到基础设施的不同部分,例如 Prometheus 和 Grafana,这是支持监控仪表盘的绝佳方式。
尽管我们发现客户在越来越多的用例中使用时间序列数据,但许多公司仍然将他们的时间序列数据类型存储在关系型数据库中。从技术角度来看,这在扩展方面并不是一个很好的选择。当只有两三个人查看仪表盘时,它可能工作良好,但当您希望组织中成千上万的人都查看相同的分析仪表盘时,关系型数据库的临时查询往往无法跟上速度。
这就是为什么我们看到 RedisTimeSeries 被用来缓存那些在较慢数据库中使用的时间序列数据,同时也能获得 Redis 的其他好处,包括选择是否持久化数据或将其保持为短暂的。