青羊区定制网站建设报价,移动互联网开发软件设计,成都彩票网站建设,有本地服务器怎么做网站目录
发布和订阅
操作命令
发布消息
订阅消息
查询订阅情况
查看活跃的频道
查看频道订阅数
使用场景和缺点
Redis Stream
Stream总述
常用操作命令
生产端
消费端
单消费者
消费组
创建消费组
消息消费
在Redis中实现消息队列
基于pub/sub
基于Stream
Re…目录
发布和订阅
操作命令
发布消息
订阅消息
查询订阅情况
查看活跃的频道
查看频道订阅数
使用场景和缺点
Redis Stream
Stream总述
常用操作命令
生产端
消费端
单消费者
消费组
创建消费组
消息消费
在Redis中实现消息队列
基于pub/sub
基于Stream
Redis中几种消息队列实现的总结
基于List的 LPUSHBRPOP 的实现
基于Sorted-Set的实现
PUB/SUB订阅/发布模式
基于Stream类型的实现
消息队列问题
Stream 消息太多怎么办?
消息如果忘记 ACK 会怎样?
PEL 如何避免消息丢失?
死信问题
Stream 的高可用
分区 Partition
Stream小结
Redis的Key和Value的数据结构组织
全局哈希表
渐进式rehash 发布和订阅
Redis提供了基于“发布/订阅”模式的消息机制此种模式下消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道( channel)发布消息订阅该频道的每个客户端都可以收到该消息。 操作命令
Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。
发布消息
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
publish channel message 返回值是接收到信息的订阅者数量如果是0说明没有订阅者这条消息就丢了再启动订阅者也不会收到。
订阅消息
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
subscribe channel [channel ...]
订阅者可以订阅一个或多个频道如果此时另一个客户端发布一条消息当前订阅者客户端会收到消息。 如果有多个客户端同时订阅了同一个频道都会收到消息。 客户端在执行订阅命令之后进入了订阅状态类似于监听只能接收subscribe、psubscribe,unsubscribe、 punsubscribe的四个命令。
查询订阅情况
查看活跃的频道
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
pubsub channels [pattern] Pubsub 命令用于查看订阅与发布系统状态包括活跃的频道是指当前频道至少有一个订阅者其中[pattern]是可以指定具体的模式类似于通配符。 查看频道订阅数
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
pubsub numsub channel 最后也可以通过 help看具体的参数运用 使用场景和缺点
需要消息解耦又并不关注消息可靠性的地方都可以使用发布订阅模式。
PubSub 的生产者传递过来一个消息Redis会直接找到相应的消费者传递过去。如果一个消费者都没有那么消息直接丢弃。如果开始有三个消费者一个消费者突然挂掉了生产者会继续发送消息另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候这断连期间生产者发送的消息对于这个消费者来说就是彻底丢失了。
所以和很多专业的消息队列系统例如Kafka、RocketMQ)相比Redis 的发布订阅很粗糙例如无法实现消息堆积和回溯。但胜在足够简单如果当前场景可以容忍的这些缺点,也不失为一个不错的选择。
正是因为 PubSub 有这些缺点它的应用场景其实是非常狭窄的。从Redis5.0 新增了 Stream 数据结构这个功能给 Redis 带来了持久化消息队列我们马上将要学习到。
Redis Stream
Redis5.0 最大的新特性就是多出了一个数据结构 Stream它是一个新的强大的支持多播的可持久化的消息队列Redis的作者声明Redis Stream地借鉴了 Kafka 的设计。
Stream总述 Redis Stream 的结构如上图所示,每一个Stream都有一个消息链表将所有加入的消息都串起来每个消息都有一个唯一的 ID 和对应的内容。消息是持久化的Redis 重启后内容还在。
具体的玩法如下
1、每个 Stream 都有唯一的名称它就是 Redis 的 key在我们首次使用xadd指令追加消息时自动创建。 消息 ID 的形式是timestampInMillis-sequence例如1527846880572-5它表示当前的消息在毫米时间戳1527846880572时产生并且是该毫秒内产生的第 5 条消息。消息 ID 可以由服务器自动生成*代表默认自动也可以由客户端自己指定但是形式必须是整数-整数而且必须是后面加入的消息的 ID 要大于前面的消息 ID。 消息内容就是键值对形如 hash 结构的键值对这没什么特别之处。
2、每个 Stream 都可以挂多个消费组每个消费组会有个游标last_delivered_id在 Stream 数组之上往前移动表示当前消费组已经消费到哪条消息了。 每个消费组都有一个Stream 内唯一的名称消费组不会自动创建它需要单独的指令xgroup create进行创建需要指定从 Stream 的某个消息 ID 开始消费这个 ID 用来初始化last_delivered_id变量。
3、每个消费组 (Consumer Group) 的状态都是独立的相互不受影响。也就是说同一份 Stream 内部的消息会被每个消费组都消费到。 4、同一个消费组 (Consumer Group) 可以挂接多个消费者 (Consumer)这些消费者之间是竞争关系任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者有一个组内唯一名称。
5、消费者 (Consumer) 内部会有个状态变量pending_ids它记录了当前已经被客户端读取,但是还没有 ack的消息。如果客户端没有 ack这个变量里面的消息 ID 会越来越多一旦某个消息被 ack它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为PEL也就是Pending Entries List这是一个很核心的数据结构它用来确保客户端至少消费了消息一次而不会在网络传输的中途丢失了没处理。
常用操作命令
生产端
xadd 追加消息
xadd第一次对于一个stream使用可以生成一个stream的结构
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xadd streamtest * name lijin age 18 *号表示服务器自动生成 ID后面顺序跟着一堆 key/value
1626705954593-0 则是生成的消息 ID由两部分组成时间戳-序号。时间戳时毫秒级单位是生成消息的Redis服务器时间它是个64位整型。序号是在这个毫秒时间点内的消息序号。它也是个64位整型。
为了保证消息是有序的因此Redis生成的ID是单调递增有序的。由于ID中包含时间戳部分为了避免服务器时间错误而带来的问题例如服务器时间延后了Redis的每个Stream类型数据都维护一个latest_generated_id属性用于记录最后一个消息的ID。若发现当前时间戳退后小于latest_generated_id所记录的则采用时间戳不变而序号递增的方案来作为新消息ID这也是序号为什么使用int64的原因保证有足够多的的序号从而保证ID的单调递增性质。
强烈建议使用Redis的方案生成消息ID因为这种时间戳序号的单调递增的ID方案几乎可以满足你全部的需求。但ID是支持自定义的。
xrange 获取消息列表会自动过滤已经删除的消息
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xrange streamtest -
其中-表示最小值 , 表示最大值 或者我们可以指定消息 ID 的列表
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xrange streamtest - 1665646270814-0 xlen 消息长度
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xlen streamtest del 删除 Stream
del streamtest 删除整个 Stream xdel可以删除指定的消息(指定ID) 消费端
单消费者
虽然Stream中有消费者组的概念但是可以在不定义消费组的情况下进行 Stream 消息的独立消费当 Stream 没有新消息时甚至可以阻塞等待。Redis 设计了一个单独的消费指令xread可以将 Stream 当成普通的消息队列 (list) 来使用。使用 xread 时我们可以完全忽略消费组 (Consumer Group) 的存在就好比 Stream 就是一个普通的列表 (list)。
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xread count 1 streams stream2 0-0
表示从 Stream 头部读取1条消息0-0指从头开始 --javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xread count 2 streams stream1 1665644057564-0
也可以指定从streams的消息Id开始(不包括命令中的消息id) --javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xread count 1 streams stream1 $ $代表从尾部读取上面的意思就是从尾部读取最新的一条消息,此时默认不返回任何消息
应该以阻塞的方式读取尾部最新的一条消息直到新的消息的到来
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xread block 0 count 1 streams stream1 $
block后面的数字代表阻塞时间单位毫秒0代表一直阻塞
此时我们新开一个客户端往stream1中写入一条消息 可以看到看到阻塞解除了返回了新的消息内容而且还显示了一个等待时间这里我们等待了10.82s
一般来说客户端如果想要使用 xread 进行顺序消费一定要记住当前消费到哪里了也就是返回的消息 ID。下次继续调用 xread 时将上次返回的最后一个消息 ID 作为参数传递进去就可以继续消费后续的消息。不然很容易重复消息基于这点单消费者基本上没啥运用场景本课也不深入去讲。
消费组
创建消费组
Stream 通过xgroup create指令创建消费组 (Consumer Group)需要传递起始消息 ID 参数用来初始化last_delivered_id变量。
0-表示从头开始消费
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xgroup create stream1 c1 0-0
$ 表示从尾部开始消费只接受新消息当前 Stream 消息会全部忽略
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xgroup create stream1 c2 $ 现在我们可以用xinfo命令来看看stream1的情况
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xinfo stream stream1 查看stream1的消费组的情况
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xinfo groups stream1 消息消费
有了消费组自然还需要消费者Stream提供了 xreadgroup 指令可以进行消费组的组内消费需要提供消费组名称、消费者名称和起始消息 ID。
它同 xread 一样也可以阻塞等待新消息。读到新消息后对应的消息 ID 就会进入消费者的PEL(正在处理的消息) 结构里客户端处理完毕后使用 xack 指令通知服务器本条消息已经处理完毕该消息 ID 就会从 PEL 中移除。
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xreadgroup GROUP c1 consumer1 count 1 streams stream1 consumer1代表消费者的名字。
表示从当前消费组的 last_delivered_id 后面开始读每当消费者读取一条消息last_delivered_id 变量就会前进。前面我们定义cg1的时候是从头开始消费的自然就获得stream1中第一条消息再执行一次上面的命令
自然就读取到了下条消息。我们将Stream1中的消息读取完很自然就没有消息可读了。
然后设置阻塞等待 我们新开一个客户端发送消息到stream1回到原来的客户端发现阻塞解除收到新消息 我们来观察一下观察消费组状态 如果同一个消费组有多个消费者我们还可以通过 xinfo consumers 指令观察每个消费者的状态
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xinfo consumers stream2 c1 可以看到目前c1这个消费者有 7 条待ACK的消息空闲了2086176ms 没有读取消息。
如果我们确认一条消息
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xack stream1 c1 1665647371850-0 就可以看到待确认消息变成了6条 xack允许带多个消息id比如 同时Stream还提供了命令XPENDING 用来获消费组或消费内消费者的未处理完毕的消息。
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
xpending stream1 c1
具体操作细节可以参考xpending 命令 -- Redis中国用户组CRUG
命令XCLAIM[kleɪm]用以进行消息转移的操作将某个消息转移到自己的Pending[ˈpendɪŋ]列表中。需要设置组、转移的目标消费者和消息ID同时需要提供IDLE已被读取时长只有超过这个时长才能被转移。 []
具体操作细节可参考xclaim 命令 -- Redis中国用户组CRUG
在Redis中实现消息队列 基于pub/sub
注意必须继承JedisPubSub这个抽象类 基于Stream java封装了两个类用于处理消息及消息的元数据。
StreamEntry和StreamEntryID Redis中几种消息队列实现的总结
基于List的 LPUSHBRPOP 的实现
足够简单消费消息延迟几乎为零但是需要处理空闲连接的问题。
如果线程一直阻塞在那里Redis客户端的连接就成了闲置连接闲置过久服务器一般会主动断开连接减少闲置资源占用这个时候blpop和brpop或抛出异常所以在编写客户端消费者的时候要小心如果捕获到异常还有重试。
其他缺点包括
做消费者确认ACK麻烦不能保证消费者消费消息后是否成功处理的问题宕机或处理异常等通常需要维护一个Pending列表保证消息处理确认不能做广播模式如pub/sub消息发布/订阅模型不能重复消费一旦消费就会被删除不支持分组消费。
基于Sorted-Set的实现
多用来实现延迟队列当然也可以实现有序的普通的消息队列但是消费者无法阻塞的获取消息只能轮询不允许重复消息。
PUB/SUB订阅/发布模式
优点
典型的广播模式一个消息可以发布到多个消费者多信道订阅消费者可以同时订阅多个信道从而接收多类消息消息即时发送消息不用等待消费者读取消费者会自动接收到信道发布的消息。
缺点
消息一旦发布不能接收。换句话就是发布时若客户端不在线则消息丢失不能寻回不能保证每个消费者接收的时间是一致的若消费者客户端出现消息积压到一定程度会被强制断开导致消息意外丢失。通常发生在消息的生产远大于消费速度时可见Pub/Sub 模式不适合做消息存储消息积压类的业务而是擅长处理广播即时通讯即时反馈的业务。
基于Stream类型的实现
基本上已经有了一个消息中间件的雏形可以考虑在生产过程中使用。
消息队列问题
从我们上面对Stream的使用表明Stream已经具备了一个消息队列的基本要素生产者API、消费者API消息Broker消息的确认机制等等所以在使用消息中间件中产生的问题这里一样也会遇到。
Stream 消息太多怎么办?
要是消息积累太多Stream 的链表岂不是很长内容会不会爆掉?xdel 指令又不会删除消息它只是给消息做了个标志位。
Redis 自然考虑到了这一点所以它提供了一个定长 Stream 功能。在 xadd 的指令提供一个定长长度 maxlen就可以将老的消息干掉确保最多不超过指定长度。
消息如果忘记 ACK 会怎样?
Stream 在每个消费者结构中保存了正在处理中的消息 ID 列表 PEL如果消费者收到了消息处理完了但是没有回复 ack就会导致 PEL 列表不断增长如果有很多消费组的话那么这个 PEL 占用的内存就会放大。所以消息要尽可能的快速消费并确认。
PEL 如何避免消息丢失?
在客户端消费者读取 Stream 消息时Redis 服务器将消息回复给客户端的过程中客户端突然断开了连接消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID。待客户端重新连上之后可以再次收到 PEL 中的消息 ID 列表。不过此时 xreadgroup 的起始消息 ID 不能为参数而必须是任意有效的消息 ID一般将参数设为 0-0表示读取所有的 PEL 消息以及自last_delivered_id之后的新消息。
死信问题
如果某个消息不能被消费者处理也就是不能被XACK这是要长时间处于Pending列表中即使被反复的转移给各个消费者也是如此。此时该消息的delivery counter通过XPENDING可以查询到就会累加当累加到某个我们预设的临界值时我们就认为是坏消息也叫死信DeadLetter无法投递的消息由于有了判定条件我们将坏消息处理掉即可删除即可。删除一个消息使用XDEL语法注意这个命令并没有删除Pending中的消息因此查看Pending消息还会在可以在执行执行XDEL之后XACK这个消息标识其处理完毕。
Stream 的高可用
Stream 的高可用是建立主从复制基础上的它和其它数据结构的复制机制没有区别也就是说在 Sentinel 和 Cluster 集群环境下 Stream 是可以支持高可用的。不过鉴于 Redis 的指令复制是异步的在 failover 发生时Redis 可能会丢失极小部分数据这点 Redis 的其它数据结构也是一样的。
分区 Partition
Redis 的服务器没有原生支持分区能力如果想要使用分区那就需要分配多个 Stream然后在客户端使用一定的策略来生产消息到不同的 Stream。
Stream小结
Stream 的消费模型借鉴了Kafka 的消费分组的概念它弥补了 Redis Pub/Sub 不能持久化消息的缺陷。但是它又不同于 kafkaKafka 的消息可以分 partition而 Stream 不行。如果非要分 parition 的话得在客户端做提供不同的 Stream 名称对消息进行 hash 取模来选择往哪个 Stream 里塞。
关于 Redis 是否适合做消息队列业界一直是有争论的。很多人认为要使用消息队列就应该采用 Kafka、RabbitMQ 这些专门面向消息队列场景的软件而 Redis 更加适合做缓存。 根据这些年做 Redis 研发工作的经验我的看法是Redis 是一个非常轻量级的键值数据库部署一个 Redis 实例就是启动一个进程部署 Redis 集群也就是部署多个 Redis 实例。而 Kafka、RabbitMQ 部署时涉及额外的组件例如 Kafka 的运行就需要再部署ZooKeeper。相比 Redis 来说Kafka 和 RabbitMQ 一般被认为是重量级的消息队列。 所以关于是否用 Redis 做消息队列的问题不能一概而论我们需要考虑业务层面的数据体量以及对性能、可靠性、可扩展性的需求。如果分布式系统中的组件消息通信量不大那么Redis 只需要使用有限的内存空间就能满足消息存储的需求而且Redis 的高性能特性能支持快速的消息读写不失为消息队列的一个好的解决方案。
Redis的Key和Value的数据结构组织
全局哈希表
为了实现从键到值的快速访问Redis 使用了一个哈希表来保存所有键值对。一个哈希表其实就是一个数组数组的每个元素称为一个哈希桶。所以我们常说一个哈希表是由多个哈希桶组成的每个哈希桶中保存了键值对数据。 哈希桶中的 entry 元素中保存了key和value指针分别指向了实际的键和值这样一来即使值是一个集合也可以通过*value指针被查找到。因为这个哈希表保存了所有的键值对所以我也把它称为全局哈希表。
哈希表的最大好处很明显就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对我们只需要计算键的哈希值就可以知道它所对应的哈希桶位置然后就可以访问相应的 entry 元素。
但当你往 Redis 中写入大量数据后就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在 的风险点那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。
当你往哈希表中写入更多数据时哈希冲突是不可避免的问题。这里的哈希冲突两个 key 的哈希值和哈希桶计算对应关系时正好落在了同一个哈希桶中。 Redis 解决哈希冲突的方式就是链式哈希。链式哈希也很容易理解就是指同一个哈希桶中的多个元素用一个链表来保存它们之间依次用指针连接。
当然如果这个数组一直不变那么hash冲突会变很多这个时候检索效率会大打折扣所以Redis就需要把数组进行扩容一般是扩大到原来的两倍但是问题来了扩容后每个hash桶的数据会分散到不同的位置这里设计到元素的移动必定会阻塞IO所以这个ReHash过程会导致很多请求阻塞。
渐进式rehash
为了避免这个问题Redis 采用了渐进式 rehash。
首先、Redis 默认使用了两个全局哈希表哈希表 1 和哈希表 2。一开始当你刚插入数据时默认使用哈希表 1此时的哈希表 2 并没有被分配空间。随着数据逐步增多Redis 开始执行 rehash。
1、给哈希表 2 分配更大的空间例如是当前哈希表 1 大小的两倍
2、把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
3、释放哈希表 1 的空间
在上面的第二步涉及大量的数据拷贝如果一次性把哈希表 1 中的数据都迁移完会造成 Redis 线程阻塞无法服务其他请求。此时Redis 就无法快速访问数据了。 在Redis 开始执行 rehashRedis仍然正常处理客户端请求但是要加入一个额外的处理
处理第1个请求时把哈希表 1中的第1个索引位置上的所有 entries 拷贝到哈希表 2 中
处理第2个请求时把哈希表 1中的第2个索引位置上的所有 entries 拷贝到哈希表 2 中
如此循环直到把所有的索引位置的数据都拷贝到哈希表 2 中。
这样就巧妙地把一次性大量拷贝的开销分摊到了多次处理请求的过程中避免了耗时操作保证了数据的快速访问。
所以这里基本上也可以确保根据key找value的操作在O1左右。
不过这里要注意如果Redis中有海量的key值的话这个Rehash过程会很长很长虽然采用渐进式Rehash但在Rehash的过程中还是会导致请求有不小的卡顿。并且像一些统计命令也会非常卡顿比如keys
按照Redis的配置每个实例能存储的最大的key的数量为2的32次方,即2.5亿但是尽量把key的数量控制在千万以下这样就可以避免Rehash导致的卡顿问题如果数量确实比较多建议采用分区hash存储。