分布式系统基础-消息队列之JMS

来源:互联网 发布:linux vi强制退出命令 编辑:程序博客网 时间:2024/05/16 07:53

JMS并没有定义消息的网络报文格式及相关的通信命令协议,但它以Java API的方式给出了一个可以纳入到J2EE环境中的消息中间件所应具备的编程级接口。同时,JMS归纳总结了两种通用的消息传递模型,深入理解这两种消息模型有助于我们准确把握消息中间件的原理和典型的使用场景。

第一个消息模型是点对点消息通信模型。如下图所示,发送方Client1(Producer/Sender) 与消费者Client 2 (Comnsumer/Receiver)通过一个特定的消息队列(Queue)联系起来,发送方产生一个消息(Message)并且将其发送到指定的Queue中,Broker随后通知消费者去处理 (Consume)此消息,消息处理完成后,消费者发送回执(Acknowledge)给MQ (通常回执的发送逻辑包括在MQ API内,不需要我们编程调用),于是MQ认为此消息已经“可靠消费/投递”,然后从其持久化存储中删除此消息。

这里写图片描述

一般情况下,消费者可以通过下面的两种方式获取新消息。

  1. Push方式:MQ收到新消息后,主动调用消费者的新消息通知接口,在这个接口方法中(onMessage)消费者完成消息的处理过程,这种方式下,消费者只能被动等待消息通知,J2EE规范里的Message Bean就是这种模式的体现。
  2. Pull方式:消费者轮询调用MQ API去获取新消息,在ActiveMQ中对应的方法是consumer.receive(),客户端的处理逻辑相对比较复杂,但拥有更多的主动权。

那么,Pull方式与Push方式的本质差别究竟体现在哪里?答案是Pull方式下消费者需要消耗一个独立的线程去拉取并处理新消息,并不占用MQ宝贵的线程资源;而在Push方式下,消息的处理过程会占用MQ的有限线程,因此Push方式很难适应极高速、高并发的消息传递场景。此外,Push模式下,当消费者离线时,MQ通常都需要持久化新消息,这需要占用大量的存储空间,当消费者恢复以后,大量积压的消息需要MQ线程去处理,因此也会拖累其他消费者。总体来说,J2EE的Push模式虽然编程简单,但由于存在上述严重问题,因此新一代的消息中间 件Kafka抛弃了 Push模式并全面拥抱了Pull模式。

第二个消息模型是发布/订阅模型。如下图所示,在这种模式下,消息生产者也被称为发布者(Publisher),消息消费者被称为订阅者(Subscriber),消息会被发送到一个名为主题(Topic)的虚拟通道中,并且每个Topic可以被多个Subscriber订阅,因此这种消息模式非常类似于“广播”或者TCP/IP中的组播。发布/订阅模型的消息传输机制是Push模式,只需保持Subscriber在线即可。

这里写图片描述

发布/订阅模型中的Subscriber通常有两种类型:持久性的和临时性的,区别在于Subscriber离线时的表现,临时性的Subscriber离线后将会错过新消息,但 MQ 会为持久性的Subscriber持久存储离线时收到的消息副本,因此,持 久 性 的Subscriber会在恢复后重新收到之前错过的消息。为什么会有这两种Subscriber的存在呢?因为发布/订阅模型主要是为了实现消息“广播”。我们知道,广播的主要目的是集中通知所有在场(在线 )的人而不是不在场的人,个别人没有被通知也不是一个严重的问题,因此发布/订阅模型中的临时性Subscriber很有价值。

在理解了 JMS的两种消息模型之后,我们来看看JMS提供的API及基本用法,如下所示是 JMS API中涉及的主要对象及收发消息的流程示意图。

这里写图片描述

首先,通 过 ConnectionFactory创建一个Connection对象,用于传输消息报文,这里采用的最重要的设计模式是工厂模式,目标是为不同的JMS Provider实现者提供统一、标准的JMS API入口,同时面向最终用户屏蔽各个不同的JMS Provider底层的不同实现,例如消息报文的格式、编码解码的逻辑、是否采用了 NIO技术及消息究竟是如何被持久化存储的等细节问题。因为几乎所有的TCP通信都会涉及会话状态保持及大量业务相关的编程逻辑,所以从SOLID设计原则中的 “单一职责原则”来看,最好将这部分逻辑从负责具体网络通信的Connection对象中剥离出来,因此我们能理解为什么有接下来的Session会话对象了。

然后通过Session对象,我们可以创建一个JMS消息并且通过Message Producer对象将其发送到指定的目标地址(Destination),也可以创建一个Message Consumer对象来从指定的目标地址上收取并处理消息。

最后,在应用不再需要收发消息后,我们可以关闭Connection,释放资源。

JMS消息系统中的两个决定性因素:消息可靠性与系统性能。这两个因素就好像硬币的正反面,既矛盾又对立统一。通常来说,消息的可靠性越高则意味着消息系统的整体吞吐量和性能越低。作为架构师,我们需要考虑系统中各类消息的特点,结合业务场景的特点,在可靠性与性能之间做出合理的折中选择。下面我们看看JMS中间件都提供了哪些可用的特性以供我们选择、决策。

首先,消息可以分为持久性消息和非持久性消息两类。持久性消息具有高可靠性的特点,可以保证“传送且只传送一次”。为了做到这一点,持久性消息通常在消息生命周期的两个关键环节中多出了 “回执” (Acknowledge)的行为,这两个环节即消息发送时(被递到消息系统时)和消息消费时(被消费方完成消费时)。非持久性消息降低了消息可靠性的等级,只保证“最多传送一次”,因此,如果MQ出现故障,则该消息可能会永远丢失,但好处是系统性能的提升。通常情况下,系统日志、告警、事件(非业务事件)都可以被定义为非持久性消息,以提升消息的吞吐量。

其次,在最影响消息投递速度的消费者回执 Acknowledge) 环节,JMS提供了多种回执模式。

  1. AUTO_ACKNOWLEDGE模式:MQ API自动逐条发送消息回执,开销最大,但可以保证消息逐条传送的可靠性。
  2. CLIENT_ACKNOWLEDGE模式:需要客户端自己编程发送回执,如果按批次发送确认,则需要的带宽开销较小,但代价是编程更复杂。
  3. DUPS_OK_ACKNOWLEDGE模式:无须编程,MQ API会采用某种最佳方式发送回执,开销最小,但代价是消费者可能收到重复的消息。
  4. NO_ACKNOWLEDGE模式:不发送回执,因此可以提供最佳性能,代价是可能会丢失消息。

最后,JMS消息中间件通常还提供了集群机制,当单机无法承载当前的设计目标时,还可以通过扩展为集群的方式来提升系统的吞吐量与性能。

参考:架构揭秘从分布式到微服务

原创粉丝点击