大多数开发人员都知道 Redis 具备实时响应请求的能力,这样的特性使其非常适合处理时间序列数据。但是时间序列数据到底是什么?业界存在各种各样不同的解释,但本文认为可以简化它的定义:
时序数据通常有一个特性:它是将用时间序列作为索引的一种数据编码,并且每个记录的时间都有一个数字值。如果将其可视化为两列,则一列将由时间索引组成,通常为 Unix 的时间戳。另一列将由某种数值组成。
非常简单。
最重要的特点是:您可以使用时间范围来分析时间序列数据,例如查看 1 月 1 日至 1 月 3 日之间发生的事件。您还可以将时间细化到秒级,甚至到毫秒级别。您还可以将数据按照不同的时间单位进行区分,例如查看每小时发生的时间。然后,如果您不想查看时间序列数据中的单个事件,则可以在此之上进行聚合,例如可以取每小时的平均值。
许多人想到时间序列数据时便会想到股票行情图,查看某只特定股票在指定时间范围内的具体趋势只是时间序列数据的一个应用。另外一种我经常使用时间序列数据的示例在我需要查看某个特定时间段内某台服务器上的 CPU 负载情况。时间序列数据也是物联网领域一种查看传感器数据或其他信息的好方法。每当您查看一段时间内的数据的趋势时,通常都需要用到某种时间序列数据库或时间序列结构。
时间序列数据在 Redis 中的发展历史
现在,让我们集中讨论 Redis 和时间序列。一切都始于 Redis 对于有序集合的支持(zset),它是 Redis 中的内置数据结构之一。人们很早就开始使用 zset 对时间序列数据使用排序集,看起来像这样:
示例中包含了一个 ZADD 命令,mySortedSets 作为键的名字,使用时间戳作为 zset 的 score,最后将需要排序的值的内容作为 zset 的成员。
这样的操作很好,但是问题在于您只能根据具体的范围来获取 zset 的值,而不能进行平均值或者降采样。
同时,集合中的元素不能重复。如果有两个值相同的元素具有相同的时间戳,则这种基于集合的操作则可能存在问题。因此,在下面的示例中,第二个值实际上会覆盖第一个值。这里就不适合使用集合来进行时间序列数据的计算
开发人员提出了许多规避的方法,这些规避的方法通常在计算上很复杂,而且比较难实现。因此必须有一种更简单的方法。
Redis Streams 特性是一个解决办法
大约两年前,Redis 4.0 首次发布了 Redis Streams 特性,该特性旨在解决系统中统一收集日志和进程间消息传递的问题。
对于时序用例来说,Redis Streams 提供了一个 Sorted Sets 中非常有用的特性。它允许对每个样本的键值对自动生成不重复的 ID。
在第一条命令中,我们将字段 myField 设置为 1000。第二条命令中,创建了一个新的入口,其中 myValue 设置为 1000,而 anotherField 设置为 hello。这些都是创建在 myStream 这条流上。
但 Redis 中的 Stream 依然缺少一些时间序列中常用的重要特性,并且它不是针对时间序列数据而设计。使用 Stream 可以获取时间范围,但功能也仅限于此。
现在我们来看看 Redis 中的 Modules 提供的 API 能够做些什么,Redis 中支持 modules 的 API 比支持 Streams 这个特性要早,在 Modules 中允许 Redis 可以支持一些扩展的数据类型。Redis 用户可以使用 Modules 来实现各种各样的扩展特性。现有的常用模块有:RediSearch、RedisGraph、RedisJSON 等等。RedisTimeSeries 模块可以在 Redis 内创建时间序列数据库。
RedisTimeSeries 模块是如何工作的?
在开始使用 RedisTimeSeries 模块之前,重要的是要了解它的原理。
首先需要了解的是“块”。实际上,您从不直接操纵“块“,但是 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 命令,而只想获取某一个具体的数据。
上面这些要求我们也可以通过 RedisTimeSeries 做到。myTS 是一个键(这是源),目标是第二个键:myTS2。在这里所有块均表示 30 秒的时间,RedisTimeSeries 会自动将它放入目标键中。所以每隔 30 秒都会有一个额外的数据样本添加到键 myTS2 中。
RedisTimeSeries 支持的不仅限于平均值。你可以用它求和,可以得到最小值,可以得到最大值,可以得到范围,可以用来计数(有多少个),获取第一个或最后一个。TSRANGE 能支持所有这些不同的聚合功能。
了解 RedisTimeSeries 中的更多命令
让我们看看 RedisTimeSeries 还能做什么。TS.INCRBY 和 TS.DECRBY 命令可以用在时间序列中的计数场景。TS.INCRBY 可以在前一个数据上增加某个值。假设您需要在 10 秒钟内收集了 10 个权重值。在一个键上执行 TS.INCRBY 命令, 这样不需要知道这个键的值。TS.DECRBY 功能相反。
TS.GET 命令可以获取最后一个值。使用 TS.ALTER 命令可以更改已创建键的元数据,包括字段和保留值等等。
TS.MRANGE 和 TS.MGET 是很有趣的命令,但解释起来有点复杂。RedisTimeSeries 可以跟踪 Redis 数据库中所有的时间序列键。TS.MRANGE 允许您可以给指定的键/值对打标签。因此,在我们的菜园的示例中,您可以获得 4 号温室的温度数据,然后使用 TS.MRANGE 查看整个 Redis 键空间中的不同键。还有很多类似的场景,TS.MGET 使您可以通过标签获取某个最新的值。您也可以将 RedisTimeSeries 与其他系统一同使用,例如 Prometheus 和 Grafana,这样是一种提高监控面板展示速度的好方法。
缓存中的 RedisTimeSeries
即使我们发现客户在越来越多的场景中使用时间序列数据,还有许多公司仍将他们的时间序列类型的数据存储在关系数据库中。从技术架构的角度来看,这样做不适合需要扩展的大规模系统。当只有一两个人在刷新监控界面查看数据时,系统可能不会出问题,但是当您希望整个公司里的成千上万的都可以同时查看一个监控数据时,关系数据库的性能就会成为瓶颈。
这就是我们为什么使用 RedisTimeSeries 来缓存时间序列数据,而不是将这些数据保存在较慢的数据库中。并且使用 Redis 还有其他好处,可以通过配置很方便的进行数据持久化。
本文转载自 中间件小哥 公众号。
原文链接:https://mp.weixin.qq.com/s/bgZEOgjxrMfZNeKt8fzuXw
评论