Part 1 - 背景
Redis作为一个灵活的高性能 key-value数据结构存储,可以用来作为数据库、缓存和消息队列。Redis 对比其他 key-value缓存产品有以下特点:
Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载到内存使用。
Redis 支持字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等数据结构的存储。
时序数据是指一串按照时间维度索引的数据,其特点是没有严格的关系模型,记录的信息可以表示成键和值的关系,因此并不需要关系型数据库进行保存。在实际应用中,时序数据通常是持续高并发写入的。针对时序数据的这一特性,Redis基于自身数据结构和扩展模块,提供了用于保存时间序列数据的两种方案:
1、基于Hash和Sorted Set数据保存时间序列数据;
2、基于RedisTimeSeries模块实现。
1.基于Hash保存时间序列数据
基于Hash保存时间序列数据的特点是可以实现对单键的快速查询,能够满足对时间序列数据的单键查询需求。Redis的Hash实现方式是将内部存储的value作为一个HashMap,并提供了用于直接存取Map成员的接口,将时间戳作为Hash集合的key,设备状态值作为Hash集合的value,因此对数据的修改和存取都可直接通过其内部Map的Key来实现操作对应属性数据,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
但是,基于Hash保存时间序列数据的短板在于无法支持对数据的范围查询,虽然时间序列是按照时间顺序插入Hash集合的,但是Hash类型的底层结构是Hash表,并没有实现对数据的有序索引,因此要对Hash类型进行范围查询,则需要扫描Hash集合中的所有数据,再将这些数据取回客户端进行排序,之后才能在客户端得到查询范围内的数据,查询效率很低。
2、基于Sorted Set保存时间序列数据
基于Sorted Set保存时间序列数据的特点是能够同时支持按时间戳范围的查询,能够根据元素的权重值来排序,在时序数据的情况下,将时间戳作为Sorted Set集合的权重值,后跟时间点上记录的测量数据,例如:<时间戳>:<测量值>。RedisSorte Set的内部使用Hash Map和SkipList来保证数据的存储有序,使用SkipList的结构可以保证具有较高的查询效率,并且在实现上比较简单。
但是,基于Sorted Set保存时间序列数据策略的短板在于其仅仅能支持范围查询,无法直接完成对时序数据的聚合计算。因此,只能先把时间范围内的数据取回到客户端,然后在客户端自行完成聚合计算。这个方法虽然能完成聚合计算,但是会带来一定的潜在风险,也就是大量数据在Redis实例和客户端间频繁传输,这会和其他操作命令竞争网络资源,导致其他操作变慢。因此SortedSets 不是一种节约内存的数据结构,其插入的时间复杂度是 O(log(N)),因此集群越大,写入耗时越长。
综合来讲,基于Hash和SortedSet保存时间序列的策略短板主要包含两个方面:其一是当执行聚合计算时,需要把数据读取到客户端内再进行聚合,当存在大量数据需要聚合时,数据传输开销大;其二是当使用该策略时,所有的数据会在两个数据类型中各保存一份,内存开销大。
2、基于RedisTimeSeries保存时间序列数据
RedisTimeSeries作为Redis的一个扩展模块,它弥补了Redis基于Hash和Sorted Set保存时间序列数据内存和数据传输开销大的缺陷,它专门面向时间序列数据提供了数据类型和访问接口,并且支持在Redis端上直接对数据进行时间范围的聚合计算。它使用固定大小的内存块作为时间序列样本,采用与Redis Streams 相同的Radix Tree来实现索引。RedisTimeSeries 的底层数据结构使用了链表,范围查询的复杂度是 O(N) 级别。这种基于RedisTimeSeries保存时间序列数据的策略具有以下特点:
保证大容量插入,低延迟读取;
按开始时间和结束时间查询;
支持任何时间桶的聚合查询(min、max、avg、sum、range、count、first、last);支持配置保留时间;
下采样/压缩-自动更新的聚合时间序列;
二级索引-每个时间序列都有标签,允许按标签查询。
Part 2 - RedisTimeSeries存储结构
RedisTimeSeries将所有的时序数据存储在chunks中。每个chunks均由双向链表中的两个相关数组组成(一个用于时间戳,一个用于样本值)。每个chunks都有预定义的样本大小,当chunks填满的时候,其他数据将自动存储到下一个chunks。chunks size可以通过参数 CHUNK_SIZE进行设置。(CHUNK_SIZE的设置必须为8的倍数,默认值:4096)
RedisTimeSeries的Key由metric指标和tags组成,其中每个Sample是时间和值的组合。标签是我们附加到数据点的键值元数据,允许我们进行分组和过滤。它们可以是字符串或数值,并在创建时添加到时间序列。
Part 3 - RedisTimeSeries的使用
当用于时间序列数据存取时,RedisTimeSeries的操作主要包含以下几个方面:
1、TS.CREATE命令
TS.CREATE命令用于创建时间序列数据集合,使用时需要设置时间序列数据集合的key和数据过期时间(以毫秒为单位)。还可以为数据集合设置标签,来表示数据集合的属性。说明:
RETENTION:选填,数据保留时间,默认:0;
ENCODING:选填,指定系列样本编码格式,分为COMPRESSED、UNCOMPRESSED两种格式;
CHUNK_SIZE:选填,块的大小;
DUPLICATE_POLICY:选填,配置对重复样本执行的操作,默认BLOCK堵塞状态。状态类型:(BLOCK,FIRST,LAST,MIN,MAX,SUM)
LABELS:必填,数据标签。
实例1:
2、TS.ADD命令
TS.ADD命令用于向时间序列集合中插入数据,其中包括时间戳和具体的数值。若先前尚未使用TS.CREATE创建时间序列,将自动创建时序数据集合。
注意:不能向最后一次使用的时间戳之前添加数据。使用 TS.ADD 命令添加值的时间戳必须要大于最后一个值的时间戳。
实例2:
也可以使用 * 让Redis将自动生成时间戳。
实例3:
TS.MADD命令用于向已存在的时间序列集合中插入新样本数据。
实例4:
3、TS.GET命令
TS.GET命令用于读取时间序列集合的最新数据。
实例5:
TS.MGET命令用于按标签查询集合中的最新数据。在使用TS.CREATE创建数据集合时,可以给集合设置标签属性。当进行查询时,就可以在查询条件中根据集合标签属性对数据样本进行匹配,其查询结果只返回满足匹配集合的最新数据。
实例6:
下列TS.MGET命令,以及FILTER设置(这个配置项用来于设置集合标签的过滤条件),查询area_id等于32的所有数据集合,并返回各自集合中最新一条数据。
4、TS.RANGE/TS.RERANGE命令
TS.RANGE/RERANGE命令用于支持时间序列集合聚合计算的范围查询;
说明:
[FILTER_BY_TS]:选填,按照时间戳过滤样本数据;
[FILTER_BY_VALUES]:选填,按照value值过滤样本数据;
[COUNT]:选填,返回样本的最大数量。
[AGGREGATION]:选填,指定要执行的聚合计算类型,RedisTimeSeries支持的聚合计算类型很丰富,包括AVG、MAX、MIN、SUM、COUNT、LAST、FIRST。
实例7:
TS.MRANGE命令通过FILTERS过滤查询跨多个时间序列的范围。
说明:
[GROUPBY]:汇总不同时间序列的结果,按提供的label名称分组;
[REDUCE]:用于聚合具有相同标签值得系列的reducer类型。
实例:
5、RedisTimeSeries其他命令
TS.DEL KEY_NAME FROM_TIMESTAMP TO_TIMESTAMP:删除给定KEY_NAME的时间戳范围内的值;
DEL KEY_NAME:删除已创建的KEY;
TS.ALTER KEY_NAME [RETENTION] LABELS:更改已创建键的元数据,包括label和保留值;
TS.INCREBY/TS.DECREBY:在最新数据上增加/减少某个值;
TS.INFO: 返回时间序列的信息和统计数据;
KEYS *:获取所有KEY;
EXISTS KEY_NAME: 检查给定KEY是否存在,若存在返回1,否返回0。
Part 4 - 总结
RedisTimeSeries作为Redis的一种扩展模块,它的出现为时序数据的存取提供了一种新方法,具有高效的查询性能,并且在存取过程中仅需很小的开销,便能够实现时序数据实时分析的愿望。