一、时间序列数据的读写特点

时间序列数据通常是持续高并发写入的。
时间序列数据的写入特点很简单,就是插入数据快,这就要求我们选择的数据类型,在进行数据插入时,复杂度要低,尽量不要阻塞。

时间序列数据读的特点:查询模式多。即要支持单点查询、范围查询和聚合计算。

Redis提供了保存时间序列数据的两种方案,分别可以基于 Hash 和 Sorted Set实现,以及基于 RedisTimeSeries 模块实现。

二、基于 Hash 和 Sorted Set 保存时间序列数据

2.1 为什么保存时间序列数据,要同时使用这两种类型?

Hash 类型的特点:可以实现对单键的快速查询,这满足时间序列数据的单键查询需求。
但 Hash 类型有个短板:它并不支持对数据进行范围查询。

Sorted Set 支持按时间戳范围查询,因为它能够根据元素的权重分数来排序。

2.1 如何保证写入 Hash 和 Sorted Set 是一个原子性的操作?

Redis 用来实现简单事务的命令:MUTIL 命令和 EXEC 命令。

  • MUTIL 命令:表示一系列原子性操作的开始。收到这个命令后,Redis 就知道,接下来再收到的命令需要放到一个内部队列中,后续一起执行,保证原子性。
  • EXEC 命令:表示一系列原子性操作的结束。一旦 Redis 收到了这个命令,就表示所有要保证原子性的命令操作都已经发送完成了。此时,Redis 开始执行刚才放到内部队列中的所有命令操作。
    image.png

2.3 如何对时间序列数据进行聚合计算?

聚合计算一般被用来周期性地统计时间窗口内的数据汇总状态,在实时监控与预警等场景下会频繁执行。

因为 Sorted Set 只支持范围查询,无法直接进行聚合计算,所以,我们只能先把时间范围内的数据取回到客户端,然后在客户端自行完成聚合计算。

这个方法虽然能完成聚合计算,但是会带来一定的潜在风险,也就是大量数据在 Redis 实例和客户端间频繁传输,这会和其他操作命令竞争网络资源,导致其他操作变慢。

为了避免客户端和 Redis 实例间频繁的大量数据传输,我们可以使用 RedisTimeSeries 来保存时间序列数据。

RedisTimeSeries 支持直接在 Redis 实例上进行聚合计算。

如果我们只需要进行单个时间点查询或是对某个时间范围查询的话,适合使用 Hash 和 Sorted Set 的组合,它们都是 Redis 的内在数据结构,性能好,稳定性高。但是,如果我们需要进行大量的聚合计算,同时网络带宽条件不是太好时,Hash 和 Sorted Set 的组合就不太适合了。此时,使用 RedisTimeSeries 就更加合适一些。

三、基于 RedisTime Series 模块保存时间序列数据

RedisTimeSeries 是 Redis 的一个扩展模块。它专门面向时间序列数据提供了数据类型和访问接口,并且支持在 Redis 实例上直接对数据进行按时间范围的聚合计算。

当用于时间序列数据存取时,RedisTimeSeries 的操作主要有 5 个:

  • 用 TS.CREATE 命令创建时间序列数据集合;
  • 用 TS.ADD 命令插入数据;
  • 用 TS.GET 命令读取最新数据;
  • 用 TS.MGET 命令按标签过滤查询数据集合;
  • 用 TS.RANGE 支持聚合计算的范围查询。

四、小结

时间序列数据的写入特点是要能快速写入,而查询的特点有三个:

  • 点查询,根据一个时间戳,查询相应时间的数据;
  • 范围查询,查询起始和截止时间戳范围内的数据;
  • 聚合计算,针对起始和截止时间戳范围内的所有数据进行计算,例如求最大 / 最小值,求均值等。

关于快速写入的要求,Redis 的高性能写特性足以应对了;而针对多样化的查询需求,Redis 提供了两种方案。

第一种方案是,组合使用 Redis 内置的 Hash 和 Sorted Set 类型,把数据同时保存在 Hash 集合和 Sorted Set 集合中。这种方案既可以利用 Hash 类型实现对单键的快速查询,还能利用 Sorted Set 实现对范围查询的高效支持,一下子满足了时间序列数据的两大查询需求。

不过,第一种方案也有两个不足:一个是,在执行聚合计算时,我们需要把数据读取到客户端再进行聚合,当有大量数据要聚合时,数据传输开销大;另一个是,所有的数据会在两个数据类型中各保存一份,内存开销不小。不过,我们可以通过设置适当的数据过期时间,释放内存,减小内存压力。

我们学习的第二种实现方案是使用 RedisTimeSeries 模块。这是专门为存取时间序列数据而设计的扩展模块。和第一种方案相比,RedisTimeSeries 能支持直接在 Redis 实例上进行多种数据聚合计算,避免了大量数据在实例和客户端间传输。不过,RedisTimeSeries 的底层数据结构使用了链表,它的范围查询的复杂度是 O(N) 级别的,同时,它的 TS.GET 查询只能返回最新的数据,没有办法像第一种方案的 Hash 类型一样,可以返回任一时间点的数据。

所以,组合使用 Hash 和 Sorted Set,或者使用 RedisTimeSeries,在支持时间序列数据存取上各有优劣势。我给你的建议是:

  • 如果你的部署环境中网络带宽高、Redis 实例内存大,可以优先考虑第一种方案;
  • 如果你的部署环境中网络、内存资源有限,而且数据量大,聚合计算频繁,需要按数据集合属性查询,可以优先考虑第二种方案。