14 | 如何在Redis中保存时间序列数据?
1、背景
记录用户在网站或者 App 上的点击行为数据,来分析用户行为。这里的数据一般包括用户 ID、行为类型(例如浏览、登录、下单等)、行为发生的时间戳:UserID, Type, TimeStamp
这些与发生时间相关的一组数据,就是时间序列数据。这些数据的特点是没有严格的关系模型,记录的信息可以表示成键和值的关系。
Redis 基于自身数据结构以及扩展模块,提供了两种解决方案。
2、时间序列数据的读写特点
时间序列数据通常是持续高并发写入的。(后面以记录设备的时时状态为例子)
这种数据的写入特点很简单,就是插入数据快,这就要求我们选择的数据类型,在进行数据插入时,复杂度要低,尽量不要阻塞。
写的特点:写的快;读的特点:查询模式多。
针对时间序列数据的“写要快”,Redis 的高性能写特性直接就可以满足了;
而针对“查询模式多”,也就是要支持单点查询、范围查询和聚合计算,Redis 提供了保存时间序列数据的两种方案:
1)、分别可以基于 Hash 和 Sorted Set 实现
2)、以及基于 RedisTimeSeries 模块实现
3、基于 Hash 和 Sorted Set 保存时间序列
Hash 和 Sorted Set 组合明显好处:它们是Redis内在的数据结构,代码成熟和性能稳定。
问题一、为什么组合两种类型?
1)、Hash 特点:可以实现单键的快速查找,满足时间序列的单键查找,可以用时间戳作为Hash的key,把记录的设备状态值作为 Hash 集合的 value。
想获取一个时间点或多个时间点温度直接用一下命令:(HGET、HMGET)
HGET device:temperature 202008030905
"25.1"
HMGET device:temperature 202008030905 202008030907 202008030908
"25.1"
"25.9"
"24.9"
Hash的弊端就是用: Hash 类型来实现单键的查询很简单。Hash 类型有个短板:它并不支持对数据进行范围查询。
2)、Sorted Set 特点:有序,根据权重排序,支持范围查找。
可以把时间戳作为 Sorted Set 集合的元素分数,把时间点上记录的数据作为元素本身。
范围查找数据:ZRANGEBYSCORE命令,输入最小时间戳、最大时间戳即可
ZRANGEBYSCORE device:temperature 202008030907 202008030910
"25.9"
"24.9"
"25.3"
"23.2"
问题二、如何保证写入 Hash 和 Sorted Set 是一个原子性的操作 ?
原子性的操作:就是指执行多个写命令操作时(例如用 HSET 命令和 ZADD命令分别把数据写入 Hash 和 Sorted Set),这些命令操作要么全部完成,要么都不完成。
也就是指,一个数据存两份,一份在hash,一份在sorted set,得保证数据一致。
这里涉及到了 Redis 的事务 MULTI 和 EXEC 命令。详细后面会学习到。
当多个命令及其参数本身无误时,MULTI 和 EXEC 命令可以保证执行这些命令时的原子性。
MULTI 命令:表示一系列原子性操作的开始。收到这个命令后,Redis 就知道,接下来再收到的命令需要放到一个内部队列中,后续一起执行,保证原子性。
EXEC 命令:表示一系列原子性操作的结束。一旦 Redis 收到了这个命令,就表示所有要保证原子性的命令操作都已经发送完成了。此时,Redis 开始执行刚才放到内部队列中的所有命令操作。
保存设备在 2020 年 8 月 3 日 9 时5 分的温度,分别用 HSET 命令和 ZADD 命令写入 Hash 集合和 Sorted Set 集合。
-MULTI
OK
-HSET device:temperature 202008030911 26.8
QUEUED
-ZADD device:temperature 202008030911 26.8
QUEUED
-EXEC
(integer) 1
(integer) 1
Redis 收到了客户端执行的 MULTI 命令。然后,客户端再执行 HSET 和 ZADD 命令后,Redis 返回的结果为“QUEUED”,表示这两个命令暂时入队,先不执行;执行了 EXEC 命令后,HSET 命令和 ZADD 命令才真正执行,并返回成功结果(结果值为 1)。
到这里就解决了单键查询、范围查询。并使用 MUTLI 和 EXEC 命令保证了 Redis 能原子性地把数据保存到 Hash 和 Sorted Set 中。
问题三、如何对时间序列数据进行聚合计算?----RedisTimeSeries
聚合计算一般被用来周期性地统计时间窗口内的数据汇总状态,在实时监控与预警等场景下会频繁执行。
Sorted Set 只支持范围查询,无法直接进行聚合计算,所以只能取一段数据传递到客户端。
大量数据在 Redis 实例和客户端间频繁传输,这会和其他操作命令竞争网络资源,导致其他操作变慢。
为了避免客户端和 Redis 实例间频繁的大量数据传输,我们可以使用 RedisTimeSeries 来保存时间序列数据。RedisTimeSeries 支持直接在 Redis 实例上进行聚合计算。
所以,如果只需要进行单个时间点查询或是对某个时间范围查询的话,适合使用 Hash 和 Sorted Set 的组合,它们都是 Redis 的内在数据结构,性能好,稳定性高。但是,如果需要进行大量的聚合计算,同时网络带宽条件不是太好时,Hash 和 Sorted Set 的组合就不太适合了。此时,使用 RedisTimeSeries 就更加合适一些。
4、基于 RedisTimeSeries 模块保存时间序列数据
RedisTimeSeries 是 Redis 的一个扩展模块。它专门面向时间序列数据提供了数据类型和访问接口,并且支持在 Redis 实例上直接对数据进行按时间范围的聚合计算。
RedisTimeSeries主要操作有五个:
1)、用 TS.CREATE 命令创建时间序列数据集合;
2)、用 TS.ADD 命令插入数据;
3)、用 TS.GET 命令读取最新数据;
4)、用 TS.MGET 命令按标签过滤查询数据集合;
5)、用 TS.RANGE 支持聚合计算的范围查询。
1)、在 TS.CREATE 命令中,我们需要设置时间序列数据集合的 key 和数据的过期时间(以毫秒为单位)。此外,我们还可以为数据集合设置标签,来表示数据集合的属性。
#创建device:temperature时间序列,数据有效期600s,600s后数据自动删除
#并且给时间序列设置了一个标签device_id:1,可以表示这个序列属于某某设备
-TS.CREATE device:temperature RETENTION 600000 LABELS device_id 1
OK
2)3)、往序列中插入数据、读取最新数据
-TS.ADD device:temperature 1596416700 25.1
1596416700
-TS.GET device:temperature
25.1
4)、按标签过滤
筛选标签不等于2的其他序列的最新一条数据
-TS.MGET FILTER device_id!=2
1) 1) "device:temperature:1"
2) (empty list or set)
3) 1) (integer) 1596417000
2) "25.3"
2) 1) "device:temperature:3"
2) (empty list or set)
3) 1) (integer) 1596417000
2) "29.5"
3) 1) "device:temperature:4"
2) (empty list or set)
3) 1) (integer) 1596417000
2) "30.1"
5)、聚合计算的范围查询
聚合计算:TS.RANGE 命令指定要查询的数据的时间范围,同时用 AGGREGATION 参数指定要执行的聚合计算类型。
RedisTimeSeries 支持的聚合计算类型很丰富,包括求均值(avg)、求最大 / 最小值(max/min),求和(sum)等。
以按照每 180s 的时间窗口,对 2020 年 8 月 3 日 9 时 5 分和 2020 年 8 月 3 日 9 时 12 分这段时间内的数据进行均值计算:
-TS.RANGE device:temperature 1596416700 1596417120 AGGREGATION avg 180000
1) 1) (integer) 1596416700
2) "25.6"
2) 1) (integer) 1596416880
2) "25.8"
3) 1) (integer) 1596417060
2) "26.1"
与使用 Hash 和 Sorted Set 来保存时间序列数据相比,RedisTimeSeries 是专门为时间序列数据访问设计的扩展模块,能支持在 Redis 实例上直接进行聚合计算,以及按标签属性过滤查询数据集合,当需要频繁进行聚合计算,以及从大量集合中筛选出特定设备或用户的数据集合时,RedisTimeSeries 就可以发挥优势了。
5、小结
Redis 保存时间序列数据。时间序列数据的写入特点是要能快速写入,
而查询的特点有三个:点查询、范围查询、聚合计算。
针对写,redis本身高性能就可以实现,
针对查询,提供两种方案:
1)、Hash + Sore Set
不足:同时存两份数据,耗内存;聚合计算需要到客户端,数据传输开销大。 不过,可以通过设置适当的数据过期时间,释放内存,减小内存压力。
优势:redis内置数据结构,高性能,支持单键精确、范围精确查询。
2)、RedisTimeSeries
不足:使用链表,O(n)复杂度;只能返回最新的数据,不能精确时间戳查询。
优势:专门为存取时间序列数据而设计的扩展模块,可以直接在redis实例上进行聚合计算
如何选择?
1)、部署环境中网络带宽高、Redis 实例内存大,可以优先考虑Hash + Sorted Set。
2)、部署环境中网络、内存资源有限,而且数据量大,聚合计算频繁,需要按数据集合属性查询,可以优先考虑RedisTimeSeries。
15 | 消息队列的考验:Redis有哪些解决方案?
1、消息队列的消息存取需求
消息队列存取消息的过程:在分布式系统中,当两个组件要基于消息队列进行通信时,一个组件会把要处理的数据以消息的形式传递给消息队列,然后,这个组件就可以继续执行其他操作了;远端的另一个组件从消息队列中把消息读取出来,再在本地进行处理。
在使用消息队列时,消费者可以异步读取生产者消息,然后再进行处理。这样一来,即使生产者发送消息的速度远远超过了消费者处理消息的速度,生产者已经发送的消息也可以缓存在消息队列中,避免阻塞生产者,这是消息队列作为分布式组件通信的一大优势。
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
需求一、消息保序
消费者需要顺序处理生产者发出的消息,否则可能会引起业务错误。(库存问题)
需求二、重复消息处理
需求三、消息可靠性保证
Redis 的 List 和 Streams 两种数据类型,就可以满足消息队列的这三个需求。
2、基于 List 的消息队列解决方案
1)、List本身先进先出,直接满足消息保序。
消费者消费订单潜在风险:List不会主动通知消费者有新消息写入,如果消费者要及时处理消息,就需要一直RPOP,可以用while(1)来实现。
即使没有新消息写入 List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。
解决:Redis 提供了 BRPOP 命令。BRPOP 命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。能节省CPU开销。
2)、解决重复消费问题:消费者程序本身能对重复消息进行判断。
一方面,消息队列要能给每一个消息提供全局唯一的 ID 号;
另一方面,消费者程序要把已经处理过的消息的 ID 号记录下来。
当收到一条消息后,消费者就可以根据消息 ID 和已经处理的消息 ID 进行对比,如果已经处理过了就不再处理,这种方式也称为幂等。
幂等性,幂等性就是指,对于同一条消息,消费者收到一次的处理结果和收到多次的处理结果是一致的。
List 需要自己生成全局 ID
#把一条全局 ID 为 101030001、库存量为 5 的消息插入了消息队列
-LPUSH mq "101030001:stock:5"
(integer) 1
3)、可靠性问题
当消费者读一条信息后,就不会留存在List了,如果消费者宕机,那么这条消息将不会被消费,重启后也看不到这条消息了
为了留存消息,Redis 提供了 BRPOPLPUSH 命令,作用是消费者在 List 读取一条消息后,会在执行 BRPOPLPUSH 命令,将消息在插入到另一个备份 List 。
这样一来,宕机的重启以后,可以从备份 List 中重新读取消息进行处理。
但是基于 List 问题:生产者消息发送很快,而消费者处理消息的速度比较慢,这就导致 List 中的消息越积越多,给 Redis 的内存带来很大压力。
解决:希望启动多个消费者程序组成一个消费组,一起分担处理 List 中的消息。但是,List 类型并不支持消费组的实现。
深度解决:Redis 5.0 提供的 Streams。
Streams 同样能够满足消息队列的三大需求。而且,它还支持消费组形式的消息读取。
3、基于 Streams 的消息队列解决方案(决定了解一下即可)
Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令。
XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
XREAD:用于读取消息,可以按 ID 读取数据;
XREADGROUP:按消费组形式读取消息;
XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。
4、小结
分布式系统组件使用消息队列时的三大需求:消息保序、重复消息处理和消息可靠性保证,这三大需求可以进一步转换为对消息队列的三大要求:消息数据有序存取,消息数据具有全局唯一编号,以及消息数据在消费完成后被删除。
Redis 是一个非常轻量级的键值数据库,部署一个 Redis 实例就是启动一个进程,部署 Redis 集群,也就是部署多个 Redis 实例。
而 Kafka、RabbitMQ 部署时,涉及额外的组件,例如 Kafka 的运行就需要再部署ZooKeeper。相比 Redis 来说,Kafka 和 RabbitMQ 一般被认为是重量级的消息队列。
个人理解:Redis不适合做消息队列。所以此节课只是简单看看了解一下,笔记基本一笔带过。