消息队列的常见模式

本文探讨了消息队列中Push和Pull两种模式的特点和应用场景。Push模式实时性强,但面临Consumer状态维护和负载问题;Pull模式利于Consumer自适应,但可能增加延迟。文中列举了不同场景下的优缺点,如Producer速率大于Consumer速率时,Pull模式能减轻Consumer压力;强调实时性时,Push模式更优。还提到了长轮询、Consumer不在线情况的处理以及动态Push/Pull策略,最后介绍了消息中间件中的Consumer实现和广播概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

push

Push即服务端主动发送数据给客户端。在服务端收到消息之后立即推送给客户端。
当 Producer 发出的消息到达后,服务端马上将这条消息投递给 Consumer。

客户端连接到broker之后,启动一个线程,这个线程的任务就是循环调用方法从broker中拉取相应的消息至本地。如果是异步方法调用,则直接调用监听器方法,间接调用业务消费消息的方法,而不使用本地内存进行消息的缓存;所以这里的异步只是客户端的异步,而非broker的主动推送。通过这种方式既能解决多客户端的连接,也能解决类似服务端的push型的消息推送。在互联网中这种实现才具有普便性,因为这种方式即解决了性能问题又解决了异步消息的需求。

Push模型最大的好处就是实时性。因为服务端可以做到只要有消息就立即推送,所以消息的消费没有“额外”的延迟。

但是Push模式在消息中间件的场景中会面临以下一些问题:

  • 在Broker端需要维护Consumer的状态,不利于Broker去支持大量的Consumer的场景

  • Consumer的消费速度是不一致的,由Broker进行推送难以处理不同的Consumer的状况

  • Broker难以处理Consumer无法消费消息的情况(Broker无法确定Consumer的故障是短暂的还是永久的)

  • 大量的推送消息会加重Consumer的负载或者冲垮Consumer

pull

Pull模式由Consumer主动从Broker获取消息。

当服务端收到这条消息后什么也不做,只是等着 Consumer 主动到自己这里来读,即 Consumer 这里有一个“拉取”的动作。

客户端(指一个connection,一般情况指一个tcp的连接建立)连接到broker之后,启动一个线程,这个线程的任务就是循环调用方法从broker中拉取相应的消息至本地。如果是同步方法调用获取则将相应的消息存放在本地内存中,当同步方法消费消息时,则从该内存区中直接获取相应的消息进行消费;

这样带来了一些好处:

  • Broker不再需要维护Consumer的状态(每一次pull都包含了其实偏移量等必要的信息)

  • 状态维护在Consumer,所以Consumer可以很容易的根据自身的负载等状态来决定从Broker获取消息的频率

场景1:Producer 速率大于 Consumer 速率

当 Producer 速率大于 Consumer 速率时,有两种可能性:一种是 Producer 本身的效率就要比 Consumer 高(例如,Consumer 端处理消息的业务逻辑可能很复杂,或者涉及到磁盘、网络等 I/O 操作);另一种是 Consumer 出现故障,导致短时间内无法消费或消费不畅。

Push 方式由于无法得知当前 Consumer 的状态,所以只要有数据产生,便会不断地进行推送,在以上两种情况下时,可能会导致 Consumer 的负载进一步加重,甚至是崩溃(例如生产者是 flume 疯狂抓日志,消费者是 HDFS+hadoop,处理效率跟不上)。除非Consumer 有合适的反馈机制能够让服务端知道自己的状况。

而采取 Pull 的方式问题就简单了许多,由于 Consumer 是主动到服务端拉取数据,此时只需要降低自己访问频率即可。举例:如前端是 flume 等日志收集业务,不断向 CMQ 生产消息,CMQ 向后端投递,后端业务如数据分析等业务,效率可能低于生产者。

场景2:强调消息的实时性

采用 Push 的方式时,一旦消息到达,服务端即可马上将其推送给消费端,这种方式的实时性显然是非常好的;而采用 Pull 方式时,为了不给服务端造成压力(尤其是当数据量不足时,不停的轮询显得毫无意义),需要控制好自己轮询的间隔时间,但这必然会给实时性带来一定的影响。

场景3:Pull 的长轮询

Pull 模式存在的问题:由于主动权在消费方,消费方无法准确地决定何时去拉取最新的消息。如果一次 Pull 取到消息了还可以继续去 Pull,如果没有 Pull 取到消息则需要等待一段时间再重新 Pull。

由于等待时间很难判定。您可能有 xx 动态拉取时间调整算法,但问题的本质在于,是否有消息到来不是由消费方决定。也许1分钟内连续到来1000条消息,接下来的半个小时却没有新消息产生,可能您的算法算出下次最有可能到来的时间点是31分钟之后,或者60分钟之后,结果下条消息10分钟后到达。

当然,延迟也有对应的解决方案,业界较成熟的做法是从短时间开始(不会对 CMQ broker 有太大负担),然后指数级增长等待。例如开始等5ms,然后10ms,然后20ms,然后40ms,以此类推,直到有消息到来,然后再回到5ms。即使这样,依然存在延迟问题:假设40ms到80ms之间的50ms消息到来,消息就延迟了30ms,对于半个小时来一次的消息,这些开销就是白白浪费的。

CMQ 提供了长轮询的优化方法,用以平衡 Pull/Push 模型各自的缺点。基本方式是:消费者如果尝试拉取失败,不是直接 return,而是把连接挂在那里 wait,服务端如果有新的消息到来,把连接拉起,返回最新消息

场景4:部分或全部 Consumer 不在线

在消息系统中,Producer 和 Consumer 是完全解耦的,Producer 发送消息时,并不要求Consumer 一定要在线,对于 Consumer 也是同样的道理,这也是消息通信区别于 RPC 通信的主要特点;但是对于 Consumer 不在线的情况,却有很多值得讨论的场景。

首先,在 Consumer 偶然宕机或下线时,Producer 的生产是可以不受影响的,Consumer 上线后,可以继续之前的消费,此时消息数据不会丢失;但是如果 Consumer 长期宕机或是由于机器故障无法再次启动,就会出现问题,即服务端是否需要为 Consumer 保留数据,以及保留多久的数据等等。

采用 Push 方式时,因为无法预知 Consumer 的宕机或下线是短暂的还是持久的,如果一直为该 Consumer 保留自宕机开始的所有历史消息,那么即便其他所有的 Consumer 都已经消费完成,数据也无法清理掉,随着时间的积累,队列的长度会越来越大,此时无论消息是暂存于内存还是持久化到磁盘上(采用 Push 模型的系统,一般都是将消息队列维护于内存中,以保证推送的性能和实时性,这一点会在后边详细讨论),都将对 CMQ 服务端造成巨大压力,甚至可能影响到其他 Consumer 的正常消费,尤其当消息的生产速率非常快时更是如此;但是如果不保留数据,那么等该 Consumer 再次起来时,则要面对丢失数据的问题。

折中的方案是:CMQ 给数据设定一个超时时间,当 Consumer 宕机时间超过这个阈值时,则清理数据;但这个时间阈值也并不容易确定。

在采用 Pull 模型时,情况会有所改善;服务端不再关心 Consumer 的状态,而是采取“你来了我才服务”的方式,Consumer 是否能够及时消费数据,服务端不会做任何保证(也有超时清理时间)。

Dynamic Push/Pull

“在Broker一直有可读消息的情况下,long-polling就等价于执行间隔为0的pull模式(每次收到Pull结果就发起下一次Pull请求)。”

这是long-polling在服务端一直有可消费消息的处理情况。在这个情况下,一条消息如果在long-polling请求返回时到达服务端,那么它被Consumer消费到的延迟是:

假设Broker和Consumer之间的一次网络开销时间为R毫秒,
那么这条消息需要经历3R才能到达Consumer
第一个R:消息已经到达Broker,但是long-polling请求已经读完数据准备返回Consumer,从Broker到Consumer消耗了R
第二个R:Consumer收到了Broker的响应,发起下一次long-polling,这个请求到达Broker需要一个R
的时间
第三个R:Broker收到请求读取了这条数据,那么返回到Consumer需要一个R的时间
所以总共需要3R(不考虑读取的开销,只考虑网络开销)

另外,在这种情况下Broker和Consumer之间一直在进行请求和响应(long-polling变成了间隔为0的pull)。

在这里插入图片描述

考虑这样一种方式,它有long-polling的优势,同时能减少在有消息可读的情况下由Broker主动push消息给Consumer,减少不必要的请求

消息中间件的Consumer实现

在消息中间件的Consumer中会有一个Buffer来缓存从Broker获取的消息,而用户的消费线程从这个Buffer中获取消费来消息,获取消息的线程和消费线程通过这个Buffer进行数据传递。
在这里插入图片描述

  • pull线程从服务端获取数据,然后写入到Buffer

  • consume线程从Buffer获取消息进行消费

有这个Buffer的存在,是否可以在long-polling请求时将Buffer剩余空间告知给Broker,由Broker负责推送数据。此时Broker知道最多可以推送多少条数据,那么就可以控制推送行为,不至于冲垮Consumer。

广播

最后来了解一下广播,先看一下广播的概念:
广播消息是指生产者产生的消息将分发给所有订阅这个消息的消费者,而普通的模式是:一批消息可以被多个人共同消费,如consumer1可能消费1,3,5记录,而consumer2可能消费的是2,4,6这种模块就是共同消费模块;而今天说的是广播消息,它是指一些消息同时被推送到多个订阅者,而这些订阅者收到的消息都是完整的,如consumer1收到的会是1,2,3,4,5,6,而consumer2回到的也会是1,2,3,4,5,6,这种就像广播一样,把消息广播给多人!

当然只有在消息到达服务器之前订阅的消费者才能收到,未订阅的消费组是无法收到消息的

### Spring Boot 消息队列使用场景与模式 #### 1. 常见消息队列模式 Spring Boot 支持多种消息队列技术,每种技术都有其适用的场景和模式。以下是常见的五种消息队列模式: - **点对点 (Point-to-Point)** 此模式下,消息由单个消费者处理。生产者将消息发送到队列中,而只有一个消费者能够接收到该消息并进行处理[^1]。 - **发布/订阅 (Publish/Subscribe)** 生产者向主题(Topic)发送消息,多个订阅者可以同时接收同一消息副本。这种模式适用于广播通知或事件驱动架构[^1]。 - **工作队列 (Work Queues)** 工作队列用于负载均衡,多个消费者共享同一个队列中的任务。此模式适合分配耗时的任务给不同的工作者节点[^1]。 - **路由 (Routing)** 路由模式允许基于特定条件分发消息至不同队列。通过绑定键(Binding Key),可以根据消息属性决定目标队列[^1]。 - **话题交换器 (Topics Exchange)** 主题交换器是一种高级形式的路由模式,它支持通配符匹配规则,使得更复杂的过滤逻辑成为可能[^1]。 --- #### 2. 消息持久化的实现方式 为了防止因服务重启或其他异常情况丢失重要数据,在实际应用中通常会启用消息持久化功能。具体做法包括设置队列和消息都为持久化状态,并确保磁盘存储可用性[^1]。 ```java // 配置 RabbitMQ 的 Queue 和 Exchange 属性以支持持久化 @Bean public Queue durableQueue() { return new Queue("durable.queue", true); // 第二参数表示是否开启持久化 } ``` --- #### 3. 延迟消息队列的设计方案 延迟消息队列可以通过以下两种方法实现: - 利用 RabbitMQ 插件 `rabbitmq_delayed_message_exchange` 来提供原生的支持; - 或者借助死信队列(Dead Letter Exchange, DLX)配合 TTL 时间戳完成延迟效果[^2]。 示例代码展示了如何发送一条具有固定延时期限的信息: ```java messageSender.sendMessage("Delayed message", 5000); Thread.sleep(10000); // 等待足够长时间让接收端捕获信息 ``` --- #### 4. Redis 实现轻量级消息队列 当项目规模较小或者不需要复杂的功能扩展时,可以选择 Redis 构建简易版 MQ 解决方案。相比传统中间件产品而言,这种方式部署简单快捷且性能优越[^3]。 配置样例如下所示: ```properties # Redis 连接基本信息 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= spring.redis.database=0 ``` --- #### 5. Apache Kafka 场景适配分析 对于高吞吐量需求的应用程序来说,Kafka 是理想的选择之一。它可以很好地满足分布式流处理的要求,比如日志收集、监控指标传输等领域内的挑战[^4]。 引入依赖项即可快速启动开发流程: ```xml <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> ``` 随后定义 Producer 和 Consumer 组件分别负责写入与读取操作。 --- #### 总结 综上所述,Spring Boot 结合不同类型的消息传递框架提供了灵活多样的选项供开发者选用。无论是追求极致效率还是注重成本控制方面都能找到合适的工具组合加以应对各种业务难题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值