消息中间件
-
概念
分布式应用之间通常有两种方式来实现系统间的通信,一种是基于远程过程调用(RPC)的方式;另一种是基于消息队列的方式。
-
RPC
RPC是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
- 它是协议,典型的RPC实现有Dubbo等。
- 网络通信的实现是透明的,调用方不需要关心网络之间的通信协议、网络I/O模型、通信的信息格式等
- 跨语言,对于调用方来说,无论接收方使用何种程序设计语言,调用都应该成功,并且返回值也应该按照调用方程序设计语言能理解的形式来描述。
-
消息队列
消息队列(MQ)是一种系统间相互协作的通信机制。
- 消息:指在应用间传送的数据,消息的表现形式是多样的,可以简单到只包含文本字符串,也可以复杂到有一个结构化的对象定义格式。
- 队列:从抽象意义上来理解,就是指消息的进和出。从时间顺序上说,进和出并不一定是同步进行的,所以需要一个容器来暂存和处理消息。因此,一个典型意义上的消息队列,至少包含消息的发送、接收和暂存功能。
-
-
消息中间件作用
-
解耦
某些业务应该作为一个独立的功能,并且这个功能并不需要关心后面不断增加的那些功能,更不需要关心后面功能的执行结果,只需要通知其他相应模块执行就可以了。简单地说,就是一个模块只需要关心自己的核心流程,而依赖该模块执行结果的其他模块如果做的不是很重要的事情,有通知即可,无需等待结果。
-
流量削峰
在某一时刻网站突然迎来用户请求高峰的情况,如果直接将用户的请求写入数据库,会导致数据访问量大到超过了原先系统的承载能力,会使系统响应延迟加剧,甚至会导致雪崩,因此可通过消息队列,先将短时间高并发的请求持久化,然后逐步处理,从而削平高峰期的并发流量,改善系统的性能。
-
日志收集
引入消息队列可以快速接收日志消息,避免因为写入日志时的某些故障导致业务系统访问阻塞、请求延迟等。
-
事务最终一致性
当系统很大时,不同的业务需要用到不同的数据库,此时便需要分布式事务来处理问题。XA(分布式事务规范):主要定义了全局事务管理器和局部事务管理器之间的接口,它充当全局事务中的协调者的角色。事务管理器控制着全局事务,管理事务生命周期并协调资源。
-
-
消息队列的功能
-
消息堆积
当消费者的处理速度跟不上生产者的发送消息的速度,就会导致消息在消息中心堆积。对于这种情况,可以给消息队列设置一个阈值,将超过阈值的消息不再放入处理中心,以防止资源被耗尽导致整个消息队列不可用。
-
消息持久化
将消息放入内存,一旦机器宕掉消息将丢失,如果场景需要消息不能丢失,势必要将消息持久化,常见的持久化方案:将消息存到本地文件,分布式文件系统、数据库系统中等。
-
可靠投递
可靠投递是不允许存在消息丢失的情况的,从消息的整个生命周期来分析,消息丢失的情况发生如下过程中:
- 从生产者到消息处理中心
- 从消息处理中心到消息消费者
- 消息处理中心持久化消息
-
消息重复
当消息发送失败或不知道是否成功时,消息的状态是待发送,定时任务会不停轮询所有待发送消息,最终保证消息不会丢失,这就带来了消息可能会重复问题。
-
严格有序
在实际的业务场景中经常会碰到按生产消息时的顺序来消费的情形,这就需要消息队列提供有序消息的保证。但顺序消费却不一定需要消息在整个产品中全局有序,有的产品可能只需要提供局部有序的保证。
-
集群
集群不仅可以让消费者和生产者在某个节点崩溃的情况下继续运行,集群之间的多个节点还能够共享负载,当某台机器或网络出现故障时能自动进行负载均衡,而且可以通过增加更多的节点来提高消息通信的吞吐量。
-
消息中间件
非底层操作系统软件、非业务应用软件,不是直接给最终用户使用的,不能直接给客户带来价值的软件统称为中间件。消息中间件关注于数据的发送和接收,利用高效可靠的异步消息传递机制集成分布式系统。中间件是一种独立的系统软件或服务程序,分布式应用系统借助这种软件在不同的技术之间共享资源,管理计算资源和网络通信。并为开发者提供了公用于所有环境的应用程序接口,当在应用程序中嵌入其函数调用时,它便可利用其运行的特定操作系统和网络环境的功能,为应用执行通信功能。
-
-
消息协议
消息协议指用于实现消息队列功能时所涉及的协议。按照是否向行业开放消息规范文档,可以将消息协议分为开放协议和私有协议。该协议的内容包括数据帧处理、信道复用、内容编码、心跳检测、数据表示和错误处理等。常见的开放协议有AMQP、MQTT、STOMP、XMPP等。
-
AMQP
-
主要概念
- Message(消息):消息服务器所处理的原子单元。消息可以携带内容,消息包含一个内容头、一组属性和一个内容体,可以对应到不同应用程序实体(传输文件、数据帧等);消息可以被保存到磁盘上,保证消息持久化;消息也可以有优先级,并且当消息必须被丢弃以确保服务器质量时,会优先丢弃优先级低的消息;消息服务器可以在内容头中添加信息,但不能修改或删除现有信息,并且不能修改内容体中的内容。
- Publisher(消息生产者):一个向交换器发布消息的客户端应用程序。
- Exchange(交换器):用来接收消息生产者所发送的消息并将这些消息路由给服务器中的队列。
- Binding(绑定):用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列链接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
- Virtual Host(虚拟主机):是消息队列以及相关对象的集合,是共享同一个身份验证和加密环境的独立服务器域。每个虚拟主机都拥有自己的队列、交换器、绑定和权限机制。
- Broker(消息代理): 表示消息队列服务器实体,接受客户端链接,实现AMQP消息队列和路由功能的过程。
- Routing Key(路由规则):虚拟机可以用它来确定如何路由一个特定消息。
- Queue(消息队列):用来保存消息直到发送给消费者,是消息的容器。
- Connection(连接):可以理解成客户端和消息队列服务器之间的一个TCP连接。
- Channel(信道):仅仅当创建了连接后,若客户端还是不能发送消息,则需要为连接创建一个信道。信道是一条独立的双向数据流通道,它是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,他们都通过信道完成。
- Consumer(消息消费者):表示一个从消息队列中取得消息的客户端应用程序。
-
核心组件的生命周期
-
消息的生命周期
一条消息的流转过程通常是:Publisher生产一条数据,发送到Broker,Broker中的Exchange可以被理解成一个规则表(Routing Key和Queue的映射关系——Binding),Broker收到消息后根据Routing Key查询投递的目标Queue。Consumer向Broker发送订阅消息时会指定自己监听哪个Queue,当有数据到达Queue时Broker会推送数据到Consumer。
-
交换器的生命周期
每台AMQP服务器都预先创建了许多交换器实例,它们在服务器启动时就存在并且不能被销毁,如果程序有特殊要求,则可以自己创建交换器,并在完成工作后进行销毁。
-
队列的生命周期
这里主要有两种消息队列的生命周期,即持久化消息队列和临时消息队列。持久化消息队列可被多个消费者共享,不管是否有消费者接收,它们都可以独立存在。临时消息队列对某个消费者是私有的,只能绑定到此消费者,当消费者断开连接时,该消息队列将被删除。
-
-
功能命令
AMQP协议文本是分层描述的,不同版本划分的层次不同,0-9版本共两层,功能层和传输层;0-10版本共三层,模型层、会话层和传输层。
-
功能层和模型层类似,都定义了一套命令,客户端应用利用这些利用这些命令来实现业务功能;
-
会话层负责将命令从客户端传递给服务器,再将服务器响应返回给客户端应用;
-
传输层负责提供帧处理、信道复用、错误检测和数据表示等。
分层的目的是在不改变协议对外提供的功能的前提下可替换各层的实现,而又不影响该层与其他层的交互。不管是两层结构还是三层结构,这里所说的功能命令实际就是协议对外提供的一套可操作的命令集合,应用程序正是基于这些命令来实现自己的业务功能的。
-
-
消息数据格式
AMQP是二进制协议,协议的不同版本在该部分的描述有所不同。所有的消息数据都被组织成各种类型的帧。所有的帧的格式都是由一个帧头、任意大小的负载和一个检测错误的结束帧字节组成。
要读取一个帧需要三步:
- 读取帧头,检查帧类型和通道
- 根据帧类型读取帧负载并进行处理
- 读取结束帧字节
-
-
MQTT
MQTT是IBM开发的一个即时通信协议,被用来当作传感器和制动器的通信协议。ActiveMQ、RabbitMQ等都是MQTT的服务端实现。
-
主要概念
一条消息的流转过程是:先由消息发布者发布消息到代理服务器,在消息中会包含主题(Topic),之后消息订阅者如果订阅了该主题的消息,将会收到代理服务器推送的消息。MQTT协议中的基本组件有:
- 网络协议(Network Connection):指客户端连接到服务器时所使用的底层传输协议,由该连接来负责提供有序的、可靠的、基于字节流的双向传输。
- 应用消息(Application Message):指通过网络所传输的应用数据,该数据一般包括主题和负载两部分。
- 主题(Topic):相当于应用消息的类型,消息订阅者订阅后,就会收到该主题的消息内容。
- 负载(Payload):指消息订阅者具体接收的内容。
- 客户端(Client):指使用MQTT的程序或设备。它可以发布应用消息给其他相关的客户端、订阅消息用以请求接收相关的应用消息、取消订阅应用消息、从服务器断开连接等。
- 服务器(Service):也是指程序或设备,它作为发送消息的客户端和请求订阅的客户端之间的中介。服务器的功能包括接收来自客户端的网络连接、接收客户端发布的应用消息、处理客户端的订阅和取消订阅请求、转发应用消息给相应的客户端等。
- 会话(Session):客户端与服务器建立连接之后就是一个会话,客户端和服务器之间通过会话来进行状态交互。会话主要用于客户端和服务器之间的逻辑层面的通信。
- 订阅(Subscription):订阅一般与一个会话关联,会话可以包含多于一个的订阅。订阅包含一个主题过滤器和一个服务质量等级。
- 主题名(Topic Name):是附加在消息上的一个标签,该标签于服务器的订阅相匹配。
- 主题过滤器(Topic Filter):是订阅中包含的一个表达式,用于表示相关联的一个或多个主题。
- MQTT控制报文(MQTT Control Packet):实际上就是通过网络连接发送的信息数据包。
-
消息数据格式
MQTT协议是通过交换预定义的MQTT控制报文来通信的,控制报文内容分为三部分:
- 固定报头(Fixed header):存在于所有控制报文中,内容包含控制报文类型、相应的标识位和剩余长度。
- 可变报头(Variable header):存在于部分控制报文中,由固定报头中的控制报文类型决定是否需要可变报头,以及可变报头的具体内容。
- 消息体(Payload):存在于部分控制报文中,表示客户端接收到的具体内容。
-
MQTT中的消息通信
MQTT协议中涉及的客户端和服务器的通信场景可分为建立连接、发布消息、主题订阅、心跳检测和断开连接。
- 建立连接:建立连接后,客户端发送给服务器的第一个报文必须是CONNECT,然后服务器需要发送CONNACK报文以响应客户端,服务端发送给客户端的第一个报文必须是CONNACK。
- 发布消息:客户端向服务器或服务器向客户端传输应用消息使用PUBLISH报文,按照消息的QoS等级会有不同的应答类型报文。MQTT中有三种QoS等级:至多一次(0)、至少一次(1)、只有一次(2)。
- 主题订阅:客户端向服务器发送SUBSCRIBE报文用于注册一个或多个感兴趣的主题,订阅报文中包含订阅者想要收到的发布报文的服务质量等级。服务器发送SUBACK报文给客户端用于确认它已经收到并且正在处理SUBSCRIBE报文。当客户端取消订阅主题时需要发送UNSUBSCRIBE报文给服务器,然后服务器发送UNSUBACK报文给客户端用于确认已经收到UNSUBSCRIBE报文。.
- 心跳检测:客户端发送PINGREQ报文给服务器,用于:
- 在没有任何其他控制报文从客户端发送给服务器时,告知服务器客户端还活着。
- 请求服务器发送响应消息确认它还活着。
- 通过网络确认网络连接没有断开,然后服务器发送PINGREQ报文,表示服务器还活着。
- 断开连接:DISCONNECT是客户端发送给服务器的最后一条控制报文,表示客户端正常断开连接。
-
-
STOMP
一个简单的文本消息传输协议,他提供了一种可互操作的连接格式,允许客户端与任意消息服务器(Broker)进行交互。ActiveMQ、RabbitMQ等都是STOMP的服务器端实现。
-
主要概念:
STOMP的客户端和服务器之间的通信是基于帧来实现的,每一帧都包含一个表示命令的字符串、一系列可选的帧头条目和帧的数据内容。帧的数据格式如下(^@表示空字节,说明帧的数据到此结束):
COMMAND header1:value1 header2:value2 Body^@
-
COMMAND:
- CONNECT:客户端通过CONNECT命令建立连接。
- CONNECTED:服务器收到CONNECT命令的请求数据并处理成功后,则返回该帧。
- STOMP:在STOMP 1.2版本中客户端还可以选择STOMP命令来建立连接,此时header中必须设置如下信息:
- accept-version:表示客户端支持的STOMP协议版本。
- host:表示客户端希望连接的虚拟主机名字。
- login:一个安全的STOMP服务器需要验证的用户标识符(可选)。
- passcode:一个安全的STOMP服务器需要验证的密码(可选)。
- heart-beat:心跳的设置(可选)。
- SEND:客户端使用该命令来发送消息,它有一个必须包含的头条目destination,用来表示把消息发送到目的地。
- SUBSCRIBE:用于客户端订阅某一个目的地的消息。跟SEND一样,也需要destination来表示订阅消息的目的地。ack头条目表示消息的确认模式。STOMP服务器收到该帧后会向客户端发送MESSAGE帧。
- UNSUBSCRIBE:客户端使用该命令移除一个已存在的订阅,该命令必须包含一个id头条目来唯一标识要取消哪一条订阅。
- BEGIN:客户端使用该命令来开启事务。帧中必须有一个头条目transaction,并且该标识还会被用在SEND、COMMIT、ABORT、ACK和NACK中,使之与该事务绑定。一个连接中不同的事务必须使用不同的标识。
- COMMIT:客户端使用该命令将一个事务提交到处理队列中。
- ABORT:客户端使用该命令终止正在执行的事务。
- ACK:是客户端在cleint和client-individual模式下确认已经收到一个订阅消息时使用的。帧中必须包含一个id头条目,头条目内容来自对应的需要确认的MESSAGE的ack。可以选择指定一个头条目transaction,表示这条消息确认动作是事务内容的一部分。
- NACK:当客户端收到一条订阅消息时,使用该命令来告诉服务器当前并没有处理该消息。
- DISCONNECTT:客户端通过该命令断开与服务器连接。
- MESSAGE:服务器通过该命令将从服务器端订阅的消息传输到客户端。在该帧中必须包含destination头条目用来表示这条消息应该发送的目的地;message-id头条目用来唯一标识发送的是哪一条消息;subscription头条目用来表示接收这条消息的订阅的唯一标识。
- RECEIPT:用于当前服务器端收到请求后需要告知客户端。在该帧中必须包含receipt-id头条目用来表示对谁的回执,receipt-id的值就是需要回执的帧所带的receipt头的值。
- ERROR:如果在连接过程中出现错误,则服务器端就会发送该帧给客户端,说明为什么拒绝请求,之后服务器端需要断开连接。
-
header:
-
content-length:所有的帧都可以有该头条目,用于表示消息体的大小,如果帧中有该头条目,则消息体最大字节数不能超过该值。
-
content-type:在SEND、MESSAGE、ERROR类型的帧中,用于描述帧数据的MINE类型,以帮助数据接收者解析帧数据。没有设置该头条目,则为二进制数据。
-
receipt:在客户端除了CONNECT命令,其他都可以为receipt设置任何值,如果设置了receipt,则服务器在确认时将会使用RECEIPT命令来处理。
-
其他header:
命令 必须的header 可选的header CONNECT accept-version,host login,passcode,heart-beat STOMP accept-version,host login,passcode,heart-beat CONNECTED version session,server,heart-beat SEND destination transaction SUBSCRIBE destination,id ack UNSUBSCRIBE id ACK id transaction NACK id transaction BEGIN transaction COMMIT transaction ABORT transaction DISCONNECTT receipt MESSAGE destination,message-id,subscription ack RECEIPT receipt-id ERROR message -
重复头条目:如果客户端或服务器收到重复的header条目,则只有第一个条目是有效的。
-
-
Body:
目前只有SEND、MESSAGE、ERROR命令字符串的帧可以包含数据(Body),其他类型的帧都不能有Body。
-
-
XMPP
XMPP(可扩展通信与表示协议)是一种基于XML的流式即使通信协议,它的特点是将上下文信息等嵌入到XML表示的结构化数据中,使得人与人之间、人与应用系统之间,以及应用系统之间能及时相互通信。XMPP协议栈中的关键协议主要包括XMPP核心定义、多媒体传输、多用户通信、发布订阅、基于HTTP的双向通信五大类。
-
基本概念
- 网络架构:XMPP消息信道涉及两个角色:客户端与服务器。一个客户端就是一个实体,客户端先和它的注册账号所在的服务器建立XML流,然后完成资源绑定,这样就能利用建好的流在客户端和服务器之间通过XML节通信了。采用客户端服务器-服务器架构的优点是把不同职责的人关注的东西分离,让客户端开发者可以专注于用户体验,而服务器开发者可以专注于可靠性和扩展性,由服务器端执行如用户认证、通道加密、防地址欺骗等安全策略。这使得该架构具备相当的健壮性,并且更容易管理。
- XMPP客户端:该架构对客户端只有很少的限制,一个XMPP客户端必须支持的功能有:
- 通过TCP套接字与XMPP服务器进行通信。
- 解析组织好的XML信息包。
- 理解消息数据类型。
- XMPP服务器:XMPP服务器的主要职责有:监听客户端连接并直接与客户端通信和与其他XMPP服务器通信。
- 地址:XMPP是在网络上通信的,所以每个XMPP实体都需要一个地址。XMPP的地址叫作JabberID,用来唯一标识XMPP网络中的各个实体。
- XML流:XMPP是基于TCP连接传输XML格式的数据进行通信的,即XMPP实体间传输的是XML流数据,它是实体间一次通信的基本数据单元。
- XML节:XML流传递XML节数据,这些XML节是一些分散的信息单元。XML节是XMPP中的一个基本语义单位。一旦建立了一个XML流,就可以通过流发送无限数量的XML节。
-
通信过程
XMPP实体间的直接网络通信发生在客户端和服务器、服务器和服务器之间。不同客户端之间的通信需要通过服务器中转。
- 从客户端到服务器
- 确定要连接的ip地址和端口号。
- 打开一个TCP连接。
- 打开一个XML流。
- 最好使用TLS来进行通道加密。
- 进行简单验证和使用安全层SASL机制来验证。
- 绑定一个资源到这个流上。
- 和网络上的其他实体交换不限数量的XML节。
- 关闭XML流。
- 关闭TCP连接。
- 从服务器到服务器
- 确定要连接的ip地址和端口号。
- 打开一个TCP连接。
- 打开一个XML流。
- 最好使用TLS来进行通道加密。
- 进行简单验证和使用安全层SASL机制来验证。
- 交换不限数量的XML节,可以在服务器之间直接交换,也可以代表每台服务器上的相关实体来交换,例如那些连接到服务器的客户端。
- 关闭XML流。
- 关闭TCP连接。
- 从客户端到服务器
-
通信过程示例
下面用一个例子来展示客户端与服务器协商XML流、交换XML节数据和关闭已协商的流的过程。服务器是im.example.com,该服务器要求使用TLS,客户端验证使用SASL SCRAM-SHA-1 机制。客户端账号是juliet@im.example.com,密码是r0m30myr0m30,客户端在这个流上提交了一个资源绑定请求。C表示客户端,S表示服务器。
-
TLS
-
客户端初始化流到服务器。
C:<stream:stream from='juliet@im.example.com' to='im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>
-
服务器发送一个应答流头给客户端来应答。
S:<stream:stream from='im.example.com' id='t7AMCin9zjMNwQKDnplntZPIDEI=' to='juliet@im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>
-
服务器发送流特性给客户端。
S:<stream:features> <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'> <requuired/> </starttls> </stream:features>
-
客户端发送STARTTLS命令给服务器。
C:<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
-
服务器通知客户端允许继续。
S:<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
-
客户端和服务器尝试通过现有的TCP连接完成TLS协商。
-
如果TLS协商成功,则客户端通过TLS保护的TCP连接初始化一个新的流到服务器。
C:<stream:stream from='juliet@im.example.com' to='im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>
-
-
SASL
-
服务器发送流头给客户端并带上任何可用的流特性来应答。
S:<stream:stream from='im.example.com' id='t7AMCin9zjMNwQKDnplntZPIDEI=' to='juliet@im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'> S:<stream:features> <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> <mechanism>SCRAM-SHA-1-PLUS</mechanism> <mechanism>SCRAM-SHA-1</mechanism> <mechanism>PLAIN</mechanism> </mechanisms> </stream:features>
-
客户端选择一种验证机制(本例中是SCRAM-SHA-1),包含初始化应答数据。
C:<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1"> biwsbj1qdWxpZ...BBQQ== </auth>
-
服务器发送challenge。
S:<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> cj1vTXNUQUF3Q...QwOTY= </challenge>
-
客户端发送一个应答。
C:<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> Yz1iaXdzLHI9b...ZKWXc9 </response>
-
服务器通知客户端成功并且包含了额外的数据。
S:<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> dj1wTk5ERlZFU...FSU289 </success>
-
客户端初始化一个新的流到服务器。
C:<stream:stream from='juliet@im.example.com' to='im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>
-
-
资源绑定
-
服务器发送一个流头到客户端并带上所支持的特性(本例中是资源绑定)。
S:<stream:stream from='im.example.com' id='gPzybzaOzBmaADgxKXu9UClbprp0=' to='juliet@im.example.com' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'> S:<stream:features> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/> </stream:features>
-
客户端绑定一个资源。
C:<iq id='yhc13a95' type='set'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <resource>balcony</resource> </bind> </iq>
-
服务器接收提交的资源绑定,并通知客户端资源绑定成功。
S:<iq idd='yhc13a95' type='result'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <jid>juliet@im.example.com/balcony</jid> </bind> </iq>
-
-
节交换
-
客户端被允许通过协商好的流发送XML节。
C:<message from='juliet@im.example.com/balcony' id='ju2ba41c' to='romeo@example.net' type='chat' xml:lang='en'> <body>Art thou not Romeo, and a Montague?</body> </message>
-
由指定的接收者做出响应,并且响应消息被传递回客户端。
E:<message from='romeo@example.net/orchard' id='ju2ba41c' to='juliet@im.example.com/balcony' type='chat' xml:lang='en'> <body>Neither, fair saint, if either thee dislike.</body> </message>
-
-
关闭
-
客户端关闭流
C:</stream:stream>
-
服务器关闭流
S:</stream:stream>
-
-
-
-
JMS
JMS(Java Message Service)即Java消息服务应用接口,是Java平台中面向消息中间件的一套规范的Java API接口,用于在两个程序之间或分布式系统中发送消息,进行异步通信。
-
体系架构
JMS将点对点和发布/订阅两种消息模型抽象成两类规范,它们相互独立。JMS的作用是提供通用接口保证基于JMS API编写的程序适用于任何一种模型,使得在更换消息队列提供商的情况下应用程序相关代码也不需要做太大的改动。
- 点对点模型
- 每条消息只有一个接收者,消息一旦接收就不再保留在消息队列中。
- 当消息发送之后,不管接收者有没有在运行,都不会影响消息被发送到队列中。
- 每条消息仅会被传送给一个接收者。
- 消息存在先后顺序(除非使用了消息优先级)。
- 当接收者收到消息时,会发生确认收到通知。
- 发布/订阅模型
- 每条消息可以有多个订阅者。
- 发布者和订阅者有时间上的依赖。发布消息后加入的订阅者无法接收加入前发布的消息。
- JMS允许订阅者创建一个可持久化的订阅,这样即使订阅者没有运行也能接收到所订阅的消息。
- 每条消息都会传送给该主题下的所有订阅者。
- 通常发布者不会知道也意识不到哪一个订阅者正在接收消息。
- 点对点模型
-
基本概念
组成:
- JMS客户端:指发送和接收消息的Java程序。
- 非JMS客户端:指使用消息系统原生的客户端API代替JMS的客户端。
- 消息:每个应用都定义了一组消息,用于多个客户端之间的消息通信。
- JMS提供商:指实现了JMS API的实际消息系统。
基本概念:
- 生产者:创建并发送消息的JMS客户端。
- 消费者:接收消息的JMS客户端。
- 客户端:生产或消费消息的基于Java的应用程序或对象。
- 队列:一个容纳被发送的等待阅读的消息的区域。
- 主题:一种支持发送消息给多个订阅者的机制。
- 消息:在JMS客户端之间传递的数据对象。包含消息头、属性、消息体三部分。
-
编程接口
- ConnectionFactory:是创建Connection对象的工厂,根据不同的消息类型用户可选择用队列连接工厂或者主题连接工厂,分别对应QueueConnectionFactory和TopicConnectionFactory。
- Destination:是一个包装了消息目的地标识符的受管对象。消息目的地是指消息发布和接收的地点,消息目的地要么是主题要么是队列。
- Connection:表示在客户端和JMS系统之间建立的连接。Connection可以生产一个或多个Session,Connection也有两种类型:QueueConnection和TopicConnection。
- Session:是实际操作消息的接口,表示一个单线程的上下文,用于发送和接受消息。在规范中Session还提供了事务的功能。Session也分为两种类型:QueueSession和TopicSession。
- MessageProducer:消息生产者由Session创建并用于将消息发送到Destination。消费者可以同步或异步接收队列和主题类型的消息。消息生产者有两种类型:QueueSender和TopicPublisher。
- MessageConsumer:消息消费者由Session创建,用于接收被发送到的Destination的消息。消息消费者有两种类型:QueueReceiver和TopicSubscriber。
- Message:消息是在消费者和生产者之间传递的对象,即将消息从一个应用程序发送到另一个应用程序。
- MessageListener:如果注册了消息监听器,那么当消息到达时将自动调用监听器的onMessage方法。
-
JMS流程
基本步骤就是首先通过设置消息队列厂商提供的参数创建连接工厂,接下来创建连接、创建会话(可开启事务)、创建队列、通过会话创建消息生产者和消息消费者、通过会话创建消息对象,然后将消息发送到相应的队列对象中,如果开启事务需要提交事务,最后关闭资源连接。
-
JMS 2.0 概述
对JMS 1.1 版本进行简化,简化的API由三个新接口构成:
- JMSContext:用于替代经典API中单独的Connection和Session。
- JMSProducer:用于替换经典API中的MessageProducer,其支持以链式操作方式配置消息传递选项、消息头和消息属性。
- JMSConsumer:用于替换经典API中的MessageConsumer,使用方式与JMSProducer类似。
经典的API没有被弃用,而是作为JMS的一部分被保留下来。二者的区别,具体包括:
- 只需创建一个JMSContext对象,而不是创建单独的Connection和Session对象。
- 在JMS1.1中,使用Connection后要用一个finally语句块来关闭Connection对象。而在JMS2.0中,JMSContext也需要调用close方法来关闭,但由于JMSContext实现了Java7中的java.lang. AutoCloseable接口,因此无需调用,会在try-with-resources语句块的结尾自动调用。
- 在JMS1.1中,创建Session对象时需要传入参数(false和Session.AUTO_ACKNOWLEDGE),指明希望创建一个非事务性会话,在该会话中收到的所有消息都将被自动确认。而在JMS2.0中,这些都是默认设置的,无需指定任何参数。如果希望指定其他会话模式(本地事务、CLIENT_ACKNOW LEDGE或DUPS_OK_ACKNOWLEDGE),只需传入一个参数即可。
- 无需创建一个TextMessage对象并将其设置为字符串,只需将字符串传入send方法即可。
- 在JMS1.1中,几乎所有方法都会抛出JMSException,该异常是已检查异常,因此必须捕获或者抛出该异常。而在JMS2.0中,抛出的异常是JMSRuntimeException,该异常是运行时异常,所以无需通过方法来显式捕获它,也不必在其throws子句中声明。
在JMS2.0中对其它API的简化包括直接从消息提取正文的新方法、直接接收消息正文的方法、创建会话的新方法、通过多种方式简化资源配置、设置传递延迟、异步发送消息、对于同一个主题订阅允许有多个使用者等。
-
-
-
RabbitMQ
-
简介
-
特点
RabbitMQ是一个由Erlang语言开发的基于AMQP标准的开源实现。具体特点包括:
- 保证可靠性。如通过持久化、传输确认、发布确认等机制来保证。
- 具有灵活的路由功能。在消息进入队列之前是通过Exchange(交换器)来路由消息。
- 支持消息集群。
- 具有高可用性。队列可以在集群中的机器上进行镜像,使得在部分节点出现问题的情况下队列仍然可用。
- 支持多种协议。RabbitMQ除支持AMQP协议之外,还通过插件的方式支持其他消息队列协议,比如STOMP、MQTT等。
- 支持多语言客户端。
- 提供管理界面。
- 提供跟踪机制。如果消息异常,使用者可以查出发生了什么情况。
- 提供插件机制。从多方面进行扩展,也可以编写自己的插件。
-
基本概念
- Message(消息):它由消息头和消息体组成。
- Publisher(消息生产者):一个向交换器发布消息的客户端应用程序。
- Exchange(交换器):用来接收生产者发送消息,并将这些消息路由给服务器中的队列。
- Binding(绑定):用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则。
- Queue(消息队列):用来保存消息直接发送给消费者。它是消息的容器,也是消息的终点。
- Connection(网络连接):比如一个TCP连接。
- Channel(信道):多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的。通过信道来复用一个TCP连接。
- Consumer(消息消费者):表示一个从消息队列中取得消息的客户端应用程序。
- Virtrual Host(虚拟主机,在RabbitMQ中叫vhost):表示一批交换器、消息队列和相关对象。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是"/"。
- Broker:表示消息队列服务器实体。
-
AMQP中的消息路由
生产者需要把消息发布到Exchange上,消息最终到达队列并被消费者接收,而Binding决定交换器上的消息应该被发送到哪个队列中。
-
交换器类型
不同类型的交换器分发消息的策略也不同,目前交换器有四种类型:Direct、Fanoiut、Topic、Headers。
- Direct交换器:是完全匹配、单播的模式。
- Fanoiut交换器:不处理路由键,只是简单地将队列绑定到交换器,发送到交换器的每条消息都会被转发到与该交换器绑定的所有队列中。通过该交换器转发消息是最快的。
- Topic交换器:通过模式匹配分配消息的路由键属性,将路由键和某种模式进行匹配,此时队列需要绑定一种模式。Topic交换器将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
-
-
工程实例
-
Java访问RabbitMQ实例
-
在Maven工程中添加依赖
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>4.10.0</version> </dependency>
-
消息生产者
public class Producer{ public static void main(String[] args) throws IOException, TimeoutException { //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setUsername("guest"); factory.setPassword("guest"); //设置RabbitMQ地址 factory.setHost("localhost"); factory.setVirtrualHost("/"); //建立到代理服务器的连接 Connection conn = factory.newConnection(); //创建信道 Channel channel = conn.createChannel(); //声明交换器 String exchangeName = "hello-exchange"; channel.exchangeDeclare(exchangeName, "direct", true); String routingKey = "testRoutingKey"; //发布消息 byte[] messageBodyBytes = "quit".getBytes(); channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes); //null处为消息的基本属性集 //关闭信道和连接 channel.close(); conn.close(); } }
-
消息消费者
public class Consumer{ public static void main(String[] args) throws IOException, TimeoutException { //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setUsername("guest"); factory.setPassword("guest"); //设置RabbitMQ地址 factory.setHost("localhost"); factory.setVirtrualHost("/"); //建立到代理服务器的连接 Connection conn = factory.newConnection(); //创建信道 final Channel channel = conn.createChannel(); //声明交换器 String exchangeName = "hello-exchange"; channel.exchangeDeclare(exchangeName, "direct", true); //声明队列(默认创建一个由Rabbit MQ命名的排他的、自动删除的、非持久化队列) String queueName = channel.queueDeclare().getQueue(); //匿名队列 String routingKey = "testRoutingKey"; //绑定队列,通过路由键testRoutingKey将队列和交换器绑定起来 channel.queueBind(queueName, exchangeName, routingKey); while(true){ //消费消息 boolean autoAck = false; //接收到消息后不应答服务器 String consumerTag = ""; channel.basicConsume(queueName, autoAck, consumerTag, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException{ String routingKey = envelope.getRoutingKey(); String contentType = properties.getContentType(); System.out.println("消费的路由键:" + routingKey); System.out.println("消费的内容类型:" + contentType); long deliveryTag = envelope.getDeliveryTag(); //确认消息 channle.basicAck(deliveryTag, false); System.out.println("消费的消息体内容:"); String bodyStr = new String(body, "UTF-8"); System.out.println(bodyStr); } }); } } }
-
启动RabbitMQ服务器
./sbin/rabbitmq-server
-
运行Consumer
-
运行Producer
先运行Consumer的main方法,再运行Producer的main方法,并发布一条消息,在Consumer的控制台就能看到接收到的消息。
-
-
Spring整合RabbitMQ
-
在Maven工程中添加依赖
<dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.0.2.RELEASE</version> </dependency>
-
Spring配置文件
<bean id="fooMessageListener" class="org.study.mq.rabbitMQ.spring.FooMessageListener" /> <!-- 配置连接 --> <rabbit:connection-factory id="connectionFactory" host="127.0.0.1" port="5672" username="guest" password="guest" virtual-host="/" requested-heartbeat="60" /> <!-- 配置RabbitTemplate --> <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="myExchange" routing-key="foo.bar" /> <!-- 配置RabbitAdmin --> <rabbit:admin connection-factory="connectionFactory" /> <!-- 配置队列名称 --> <rabbit:queue name="myQueue" /> <!-- 配置Topic类型交换器 --> <rabbit:topic-exchange name="myExchange"> <rabbit:bandings> <rabbit:banding queue="myQueue" pattern="foo.*" /> </rabbit:bandings> </rabbit:topic-exchange> <!-- 配置监听器 --> <rabbit:listener-container connection-factory="connectionFactory"> <rabbit:listener ref="fooMessageListener" queue-names="myQueue" /> </rabbit:listener-container>
spring-rabbit的主要API如下:
- MessageListenerContainer:用来监听容器,为消息入队提供异步处理。
- RabbitTemplate:用来发送和接收消息。
- RabbitAdmin:用来声明队列、交换器、绑定。
-
发送消息
public class SendMessage{ public static void main(final String... args) throws Exception{ AbstractApplicationContext ctx = new ClassPathXmlApplicationContext("spring-context.xml"); RabbitTemplate template = ctx.getBean(RabbitTemplate.class); template.convertAndSend("Hello World"); ctx.close(); } }
-
消费消息
public class FooMessageListener implements MessageListener{ public void onMessage(Message message){ String messageBody = new String(message.getBody()); System.out.println("接收到消息 '" + messageBody + "'"); } }
-
运行SendMessage
运行SendMessage类的main方法,在控制台将看到打印出接收到的消息’Hello World’。
-
-
基于RabbitMQ的异步处理
下面是基于Sping框架改造的发送邮件代码实现异步的例子。
-
在Maven工程中添加依赖
<dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.0.2.RELEASE</version> </dependency>
-
Spring配置文件
<bean id="mailMessageListener" class="org.study.mq.rabbitMQ.async.MailMessageListener" /> <!-- 配置连接 --> <rabbit:connection-factory id="connectionFactory" host="127.0.0.1" port="5672" username="guest" password="guest" virtual-host="/" requested-heartbeat="60" /> <bean id="jsonMessageConverter" class="org.springframework.amqp.support.sonverter.Jackson2JsonMessageConverter" /> <!-- 配置RabbitTemplate --> <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="mailExchange" routing-key="mail.test" message-converter="jsonMessageConverter" /> <!-- 配置RabbitAdmin --> <rabbit:admin connection-factory="connectionFactory" /> <!-- 配置队列名称 --> <rabbit:queue name="mailQueue" /> <!-- 配置交换器 --> <rabbit:topic-exchange name="mailExchange"> <rabbit:bandings> <rabbit:banding queue="mailQueue" pattern="mail.*" /> </rabbit:bandings> </rabbit:topic-exchange> <!-- 配置监听器 --> <rabbit:listener-container connection-factory="connectionFactory"> <rabbit:listener ref="mailMessageListener" queue-names="mailQueue" /> </rabbit:listener-container>
-
发送消息
public class Business{ //用户注册 public void userRegister(){ //校验用户填写的信息是否完整 //将用户及相关信息保存到数据库 //注册成功后发送一条消息表示要发送邮件 AbstractApplicationContext ctx = new ClassPathXmlApplicationContext("async-context.xml"); RabbitTemplate template = ctx.getBean(RabbitTemplate.class); Mail mail = new Mail(); mail.setTo("12345678@qq.com"); mail.setSubject("我的一封邮件"); mail.setContent("我的邮件内容"); template.converterAndSend(mail); ctx.close(); } public static void main(final String... args) throws Exception{ Business business = new Business(); business.userRegister(); } }
-
消费消息
public class MailMessageListener implements MessageListener{ public void onMessage(Message message){ String body = new String(message.getBody()); ObjectMapper mapper = new ObjectMapper(); try{ Mail mail = mapper.readValue(body, Mail.class); System.out.println("接收到邮件消息:" + mail); sendEmail(mail); } catch (IOException e) { e.printStackTrace(); } } public void sendEmail(Mail mail){ //调用 JavaMail API 发送的邮件 } }
-
运行Business
运行Business类的main方法,在控制台将看到打印出接收到的消息。
-
-
基于RabbitMQ的消息推送
HTML5定义了WebSocket,它能够实现浏览器与服务器之间全双工通信。其优点有两个:一是服务器与客户端之间交换的标头信息很小;二是服务器可以主动传送数据给客户端。RabbitMQ有丰富的第三方插件,针对WebSocket通信RabbitMQ提供了Web STOMP插件,它是一个实现了STOMP协议的插件,启用插件后,浏览器就可以使用WebSocket与之通信了。当有新消息需要发布时,系统后台将消息数据发送到RabbitMQ中,再通过WebSocket将数据推送给浏览器。
-
-
RabbitMQ实践建议
-
虚拟主机
虚拟主机是AMQP协议中的一个基本概念,客户端在连接消息服务器时必须指定一个虚拟主机。可通过RabbitMQ提供的rabbitmqctl工具管理vhost:
#创建虚拟主机testrabbitmqctl add_vhost test #删除虚拟主机testrabbitmqctl delete_vhost test #查询当前RabbitMQ服务器中所有虚拟机rabbitmqctl list_vhosts
-
消息保存
-
Queue持久化
通过设置durable为true来实现的。
-
Message持久化
通过设置durable为true只能保证队列的持久化,并不能保证队列中的消息也能持久化,通过看源码会发现消息持久化就是将BasicProperties中的deliveryMode设置为2。
-
Exchange持久化
同样,如果没有设置Exchange持久化,服务器重启之后Exchange就不存在了。所以一般建议在将消息持久化时,也要设置Exchange持久化。在声明Exchange时使用支持durable入参的方法,将其设置为true即可。
注:即使对以上三种组件都设置了持久化,也不能保证消息在使用过程中完全不会丢失。
-
-
消息确认模式
在默认情况下,生产者把消息发送出去之后,Broker是不会返回任何信息给生产者的,为了防止消息的丢失,RabbitMQ提供了两种解决方式,一是通过AMQP协议中的事务机制;二是把信道设置成确认模式。而在AMQP中当信道设置成事务模式后,生产者和Broker之间会有一种发送/响应机制判断当前命令操作是否可以继续,但一般不建议使用事务模式,而是采用性能更好的发送确认模式。发送确认模式是RabbitMQ对AMQP的扩展实现,把信道设置成确认模式之后,在该信道上发布的所有消息都会被分配唯一id,一旦被投放到所匹配的队列中,该信道就会向生产者发送确认消息,在确认消息中包含了之前的id,从而让生产者知道消息已到达目的队列。发送确认最大的优势是异步,由于没有事务回滚的概念,其对Broker的性能影响相对来说也较小。
- 普通确认:每发送完一条消息后,就调用waitForConfirms方法等待Broker确认消息,本质上这就是串行方式确认。
- 批量确认:每发送完一批消息后,就调用waitForConfirms方法等待Broker确认消息。
- 异步确认:通过调用addConfirmListener方法注册回调,在Broker确认了一条或多条消息之后由客户端回调该方法。
-
消费者应答
-
两种消息回执模式
在AMQP中定义了两种消息回执方式:自动回执和手动回执。在自动回执模式下,当Broker成功发送消息给消费者后就会立即把此消息从队列中删除,而不用等待消费者回送确认消息。而在手动回执模式下,当Broker成功发送消息给消费者后并不会立即把此消息删除,而是要等收到消费者回送的确认消息后才删除。
-
拒绝消息
当消费者处理消息失败或者当前不能处理该消息时,可以给Broker发送一个拒绝消息的指令,并且可要求Broker将该消息丢弃或重新放入队列中。拒绝消息有两种方式,一是拒绝一条消息;二是拒绝多条消息。
-
消息预取
为了防止有些消费者一直很忙,而有些消费者处理很快并一直清闲,可以通过设置预取数量来限制每个消费者在收到下一个确认回执前一次最多可以接收多少条消息。可通过Cannel接口中的basicQos方法的perfetchCount的值来设置预取数量。不要设置无限制的预取数量,这将导致内存耗尽并崩溃,然后所有消息又被重新发送。
-
-
流控机制
在RabbitMQ中如果生产者持续高速发送消息,而消费者消费的速度又低于生产者发送的速度,若没有流控很快会使进程邮箱mailbox达到阈值限制,从而阻塞生产者的操作(因为有Block机制进程不会崩溃),然后RabbitMQ会进行换页操作,把内存中数据持久化到磁盘上。
为了解决这个问题,RabbitMQ使用了一种基于信用证的流控机制。当服务器出现内存或磁盘等资源的使用量达到所设置的阈值情况时,就会触发流控机制,从而阻塞消息生产端的连接,阻止生产者继续发送消息,直到资源不足的警告解除。
-
通道
消息客户端和消息服务器之间的通信是双向的,保持它们之间的网络连接是很耗费资源的,为了不占用大量TCP/IP连接的情况下也能有大量的连接,AMQP增加了通道的概念,RabbitMQ支持并鼓励一个连接中建立多个通道,因为相对来说,建立和销毁通道的代价会小很多。尽量避免在线程之间共享通道,尽量保持每个线程有单独的通道。
-
-
-
RocketMQ
-
简介
-
特点
- 具有灵活的可扩展性。
- 具有海量消息堆积能力。
- 支持顺序消息。分为全局有序消息和局部有序消息。推荐使用局部有序消息。
- 支持多种消息过滤方式。分为服务器端过滤和消息端过滤。在服务器端过滤的优点是减少了不必要的消息传输,缺点是增加了消息服务器的负担,实现相对复杂。消费端过滤则完全由具体应用自定义实现,这种方式更加灵活,缺点是很多无用的消息会被传输给消息消费者。
- 支持事务消息。这一特性对于分布式事务来说提供了另一种解决思路。
- 支持回溯消息。回溯消息指对于消费者已经消费成功的消息,由于业务需求需要重新消费。RocketMQ支持按照时间回溯消息,可以向前回溯也可以向后回溯。
-
基本概念
- 生产者:负责生产消息。RocketMQ提供了三种方式发送消息:同步、异步和单向。
- 同步发送:指消息发送方发送数据后,会在接收方发回的响应之后才发送下一个数据包。
- 异步发送:指消息发送方发送数据后,不等接收方发回响应,就接着发送下一个数据包。
- 单向发送:指只负责发送消息而不等待服务器回应且没有回调函数触发。
- 生产者组:是一类生产者的集合,这类生产者通常发送一类消息并且发送逻辑一致,所以将这些生产者分组在一起。
- 消费者:负责消费消息。从消息服务器拉取消息并将其输入用户应用程序中。从用户应用的角度来看,消费者有两种类型:拉取型消费者和推送型消费者。
- 拉取型消费者:主动从消息服务器拉取消息,只要批量拉取到消息,用户应用就会启动消费过程,所以Pull被称为主动消费类型。
- 推送型消费者:封装了消息的拉取、消费进度和其他内部维护工作,将消息到达时执行的回调接口留给用户应用程序实现。
- 消费者组:是一类消费者的集合,这类消费者通常消费同一类消息,并且消费逻辑一致,所以将这些消费者分组在一起。
- 消息服务器:是消息存储中心,其主要作用是接收来自生产者的消息并进行存储,消费者从这里拉取消息。它还存储与消息相关的元数据,包括用户组、消费进度偏移量、队列信息等。
- 单Master:一旦Broker重启或宕机就会导致整个服务不可用。不建议使用。
- 多Master:所有消息都是Master没有Slave,这种方式优点是配置简单,单个Master宕机或重启维护对应用无影响;缺点是在单台机器宕机期间,该机器上未被消费的消息在机器恢复之前不可订阅,消息的实时性会受到影响。
- 多Master多Slave(异步双写):为每个Master都配置一个Slave,消息采用异步双写的方式,主备都写成功了才返回成功。这种方式的优点是数据与服务都没有单点问题,Master宕机时消息无延迟,服务与数据的可用性非常高;缺点是相对异步复制方式其性能略低,发送消息的延迟略高。
- 多Master多Slave(异步复制):为每个Master都配置一个Slave,消息采用异步复制方式,主备之间有毫秒级消息延迟。这种方式的优点是丢失的消息非常少,且消息的实时性不会受到影响,Master宕机后消费者可以继续从Salve消费,中间的过程对用户应用程序透明,不需要人工干预,性能同多Master方式几乎一样;缺点是Master在宕机后在磁盘损坏的情况下会丢失极少量的消息。
- 名称服务器:用来保存Broker相关元信息并给生产者和消费者查找Broker信息。每个Borker在启动时都会到名称服务器中注册,生产者在发送消息前会根据主题到名称服务器中获取到Broker的路由信息,消费者也会定时获取主题的路由信息。功能类似Zookeeper。
- 消息:要传输的信息。一条消息必须有一个主题,主题可以被看作是信件要邮寄的地址。一条消息也可以拥有一个可选的标签和额外的键值对,它们被用于设置一个业务key并在Broker上查找此消息,以便在开发期间查找问题。
- 主题:可以被看作是消息的归类,它是消息的第一级类型。一条消息必须有一个主题。
- 标签:可以被看作是子主题,它是消息的第二级类型。用于为用户提供额外的灵活性,一条消息可以没有标签。
- 队列:主题被划分为一个或多个子主题,即队列。在一个主题下可以设置多个队列,在发送消息时执行该消息的主题,RocketMQ会轮询该主题下的所有队列将消息发送出去。
- 消息消费模式:分为集群消费和广播消费。默认是集群消费,表示一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉了,分组内的其他消费者会接替挂掉的消费者继续消费。而广播消费会将消息发送给消费者组中的每一个消费者进行消费。
- 消息顺序:分为顺序消费和并行消费。顺序消费表示消息消费的顺序同生产者为每个消息队列发送的顺序一致。并行消费不再保证消息顺序,消费的最大并行数量受每个消费者客户端指定的线程池限制。
- 生产者:负责生产消息。RocketMQ提供了三种方式发送消息:同步、异步和单向。
-
-
工程实例
-
Java访问RacketMQ实例
-
引入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.2.0</version> </dependency>
-
消息生产者
public class Producer{ public static void main(String[] args) throws Exception{ //创建一个消息生产者,并设置一个消息生产者组 DefaultMQProducer producer = new DefaultMQProducer("niwei_producer_group"); //指定NameServer地址 producer.setNamesrvAddr("localhost:9876"); //初始化Producer,在整个应用生命周期中只需要初始化一次 producer.start(); for(int i = 0; i < 100; i++){ //创建一个消息对象指定其主题、标签和消息内容 Message msg = new Message( "topic_example_java" /* 消息主题名 */, "TagA" /* 消息标签 */, ("Hello Java demo RocketMQ" + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* 消息内容 */ ); //发送消息并返回结果 SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } //一旦生产者实例不再被使用,则将其关闭,包括清理资源、关闭网络连接等 producer.shutdown(); } }
-
消息消费者
public class Consumer{ public static void main(String[] args) throws Exception{ //创建一个消息生产者,并设置一个消息生产者组 DefaultMQConsumer consumer = new DefaultMQConsumer("niwei_consumer_group"); //指定NameServer地址 consumer.setNamesrvAddr("localhost:9876"); //设置Consumer第一次启动时是从队列头部还是队列尾部开始消费的 consumer.setConsumerFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //订阅指定Topic下的所有消息 consumer.subscribe("topic_example_java", "*"); //注册消息监听器 consumer.registerMessageListener((List<MessageExt> list, ConsumeConcurrentlyContext context) -> { //默认list里只有一条消息,可以通过设置参数来批量接收消息 if (list != null) { for (MessageExt ext : list) { try{ System.out.println(new Date() + new String(ext.getBody, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); //消费者对象在使用之前必须要调用start方法初始化 consumer.start(); System.out.println("消息消费者已启动"); } }
-
启动NameServer
#启动NameServernohup sh bin/mqnamesrv & #跟踪NameServer输出的日志文件tail -f ~/logs/rocketmqlogs/namesrv.log
-
启动Broker
#启动Brokernohup sh bin/mqbroker -n localhost:9876 & #跟踪Broker输出的日志文件tail -f ~/logs/rocketmqlogs/broker.log
-
运行Consumer
运行Consumer类,当看到控制台打印出消息消费者已启动,说明启动成功。
-
运行Producer
再运行Producer类,在Consumer的控制台能看到接收的消息。
-
-
Spring整合RacketMQ
-
消息生产者
public class SpringProducer{ private Logger logger = Logger.getLogger(getClass()); private String producerGroupName; private String nameServerAddr; private DefaultMQProducer produccer; public SpringProducer(String producerGroupName, String nameServerAddr){ this.producerGroupName = producerGroupName; this.nameServerAddr = nameServerAddr; } public void init() throws Exception{ logger.info("开始启动消息生产者服务..."); //创建一个消息生产者,并设置一个消息生产者组 producer = new DefaultMQProducer(producerGroupName); //指定NameServer地址 producer.setNamesrvAddr(nameServerAddr); //初始化SpringProducer,在整个应用生命周期中只需要初始化一次 producer.start(); logger.info("消息生产者服务启动成功."); } public void destroy(){ logger.info("开始关闭消息生产者服务..."); producer.shutdown(); logger.info("消息生产者服务已关闭."); } public DefaultMQProducer getProducer(){ return producer; } }
-
消息消费者
public class SpringConsumer{ private Logger logger = Logger.getLogger(getClass()); private String consumerGroupName; private String nameServerAddr; private String topicName; private DefaultMQConsumer consumer; private MessageListenerConsumer messageListener; public SpringConsumer(String consumerGroupName, String nameServerAddr, String topicName, MessageListenerConsumer messageListener){ this.consumerGroupName = consumerGroupName; this.nameServerAddr = nameServerAddr; this.topicName = topicName; this.messageListener = messageListener; } public void init() throws Exception{ logger.info("开始启动消息消费者服务..."); //创建一个消息消费者,并设置一个消息消费者组 consumer = new DefaultMQConsumer(consumerGroupName); //指定NameServer地址 consumer.setNamesrvAddr(nameServerAddr); //设置Consumer第一次启动时是从队列头部还是队列尾部开始消费的 consumer.setConsumerFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //订阅指定Topic下的所有消息 consumer.subscribe(topicName, "*"); //注册消息监听器 consumer.registerMessageListener(messageListener); //消费者对象在使用之前必须要调用start方法初始化 consumer.start(); logger.info("消息消费者服务启动成功."); } public void destroy(){ logger.info("开始关闭消息消费者服务..."); consumer.shutdown(); logger.info("消息消费者服务已关闭."); } public DefaultMQPushConsumer getConsumer(){ return consumer; } }
消息监听器类
public class MessageListener implements MessageListenerConcurrently{ private Logger logger = Logger.getLogger(getClass()); public ConsumerConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumerConcurrentlyContext consumeConcurrentlyContext){ if(list != null){ for(MessageExt ext : list){ try{ logger.info("监听到消息:" + new String(ext.getBody(), "UTF-8")); } catch(UnsupportedEncodingException e){ e.printStackTrace(); } } } return ConsumerConcurrentlyStatus.CONSUME_SUCCESS; } }
-
Spring配置文件
<!-- 消息生产者配置 --> <bean id="producer" class="org.study.mq.rocketMQ.spring.SpringProducer" init-method="init" destroy-method="destroy"> <constructor-arg name="nameServerAddr" value="localhost:9876" /> <constructor-age name="producerGroupName" value="spring_producer_group" /> </bean> <!-- 消息消费者配置 --> <bean id="messageListener" class="org.study.mq.rocketMQ.spring.MessageListener" /> <bean id="consumer" class="org.study.mq.rocketMQ.spring.SpringConsumer" init-method="init" destroy-method="destroy"> <constructor-arg name="nameServerAddr" value="localhost:9876" /> <constructor-age name="consumerGroupName" value="spring_consumer_group" /> <constructor-arg name="topicName" value="spring-rocketMQ-topic" /> <constructor-arg name="messageListener" ref="messageListener" /> </bean>
-
-
基于RacketMQ的消息顺序处理
RocketMQ是如何确保消息的严格顺序的?
在RocketMQ消息服务器内部有多个主题,而在每个主题内部又有不同的队列,当生产者发送消息时,默认是采用轮询的方式发送到不同的队列中,此时消息消费者会被分配到多个队列中,然后多个队列再同时拉取数据消费,所以对外表现出来的消息消费不是顺序的。
而顺序消费的原理是确保将消息投递到同一队列中,在队列内部RocketMQ保证先进先出。而同一个队列会被投递到同一个消费者实例,再由消费者拉取数据进行消费。在消费者内部会维护本地队列锁,以保证当前只有一个线程能够进行消费,所拉到的消息先被存入消息处理队列中,然后再从消息处理队列中顺序获取消息用MessageListenerOrderly进行消费。
RocketMQ默认提供了两种MessageQueueSelector的实现算法,SelectMessageQueueByRandom和SelectMessageQueueByHash(随机和哈希),可以根据业务自定义选择队列算法。
-
基于RacketMQ的分布式事务
所谓事务消息就是基于消息中间件模拟的两阶段提交(2PC),属于对消息中间件的一种特殊利用。总体思路:1.系统A先向消息中间件发送一条预备消息,消息中间件在保存好消息之后向系统A发送确认消息;2.系统A执行本地事务;3.系统A根据本地事务执行结果再向消息中间件发送提交消息,以提交二次确认;4.消息中间件收到提交消息后,把预备消息提交为可投递,订阅者最终将接收到该消息。
对于以上四步,每一步都可能产生错误:
- 第1步出错,整个事务失败,不会执行到后面系统A的本地事务
- 第2步出错,整个事务失败,不会执行到后面系统A的本地事务
- 第3步出错,系统A会实现一个消息回查接口,MQ服务端在得不到系统A反馈时会轮询该消息回查接口,检查系统A的本地事务执行结果,如果成功继续第4步,如果失败则回滚到第1步中发送预备消息。
- 第4步出错,此时系统A的本地事务已经提交成功,MQ服务端通过回查接口能够检查到该事务执行成功,那么由MQ服务端将预备消息标记为可投递,从而完成整个消息事务的处理。
RocketMQ的事务消息在设计上借鉴了2PC的理论,其整体流程如下:
- 事务发起方首先发送prepare(预备)消息到MQServer。
- MQServer向事务发起方ACK确认消息发送成功。
- 事务发起方接收到确认消息后执行本地事务。
- 事务发起方根据本地事务的执行结果(提交或回滚),从而向MQServer提交二次确认。如果是回滚,MQ将删除该prepare消息不进行下发;如果是提交,则MQ将会把该消息发送给消费者。
- 如果在执行本地事务中该应用挂掉或超时,那么第四步的二次确认消息最终没有到达MQServer,则MQServer将在经过一定时间后对该消息发起消息回查,通过不停的询问同组的的其他生产者来获取状态。
- 发送方接收到回查消息后,查询对应消息的本地事务执行结果。
- 根据回查得到的本地事务的最终执行结果再次提交二次确认。
- 消费端的消费成功机制则是由MQ保证的。
-
-
RocketMQ实践建议
-
消息重试
-
生产者端重试
//创建一个消息生产者,并设置一个消息生产者组 DefaultMQProducer producer = new DefaultMQProducer("niwei_producer_group"); //消息发送失败重试次数 producer.setRetryTimesWhenSendFailed(3); //消息没有存储成功,是否发送到另外一个 Brokerproducer.setRetryAnthorBrokerWhenNotStoreOK(true); //指定NameServer地址producer.setNamesrvAddr("localhost:9876"); //初始化TransactionSpringProducer,在整个应用生命周期内只需要初始化一次 producer.start();
-
消费者端重试
//创建一个消息消费者,并设置一个消息消费者组 consumer = new DefaultMQConsumer("niwei_consumer_group"); //指定NameServer地址consumer.setNamesrvAddr("localhost:9876"); //设置Consumer第一次启动时是从队列头部还是队列尾部开始消费的 consumer.setConsumerFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //订阅指定Topic下的所有消息 consumer.subscribe("topic_example_java", "*"); //注册消息监听器 consumer.registerMessageListener((List<MessageExt> list, ConsumeConcurrentlyContext context) -> { for (MessageExt message : list) { String messageBody = new String(message.getBody()); if(message.getReconsumeTimes() == 3){ //如果重试了3次还是失败,则不再重试 //把重试次数达到3次的消息选择记录下来 saveRecsumeStillMessage(message); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }else{ try{ doBusiness(messageBody); } catch (ReconsumeException e) { //业务方法在执行时如果返回的是可以再消费的异常,则触发重试 return ConsumeConcurrentlyStatus.RECONSUME_LATER; } } } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}); //消费者对象在使用之前必须要调用start方法初始化consumer.start();
-
-
消息重复
RocketMQ能保证消息至少一次被投递,即支持至少一次模式,但不支持只有一次模式。因为要在分布式系统环境下实现发送不重复并且消费不重复,将会产生非常大的开销,RocketMQ为了追求高性能并没有支持此特性,导致在消息消费时将可能收到重复的消息。通常的做法是要求在消息消费时去重,也就是消费消息要做到幂等性,只要保持幂等性,不管来多少条重复消息,最后处理的结果都是一样的。当然还有另一种解决方案,就是首先保证每条消息都有一个唯一标识,然后用一个消息处理日志表来记录已经处理成功的消息ID,加入新到的消息ID已在日志表中,则不再处理这条消息。
-
集群
-
名称服务器集群
提供了轻量级的服务发现和路由,每个名称服务器都记录了完整的路由信息,提供了相应的读写服务,支持快速存储扩展。名称服务器是一个几乎无状态的节点,节点之间并没有信息同步,所以其集群功能方案也很简单。对于消息服务器,一般直接在其配置文件中指定;对于生产者、消费者,可以在代码中通过调用相应的setNamesrvAddr方法来设置。
-
Broker集群
Broker的节点实例分为Master和Slave两种,一个Master可以对应多Slave,但一个Slave只能有一个Master。生产者的发生机制将保证消息尽量被平均分摊到所有队列中,而队列会被平均分散到多个Broker实例上,从而实现负载均衡的。队列并不存消息,而是存CommitLog文件的位置信息,消息又被存储在CommitLog文件中,从而通过队列找到存在CommitLog中的消息。
刷盘,指刷新磁盘,就是将内存中的数据保存到磁盘中。在RcoketMQ中有同步刷盘和异步刷盘,同步是指生产者发送的消息都要等消息数据保存到磁盘成功后才向生产者返回成功信息,这样可以避免消息丢失但磁盘I/O开销大。而异步是指生产者发送的消息先缓存起来就向生产者发送成功信息,然后再将缓存的数据异步保存到磁盘中,这样可能导致少量消息丢失但性能高。
-
生产者集群
生产者端集群比较简单只需要部署多个生产者应用实例即可。默认情况下采用轮询的方式选择队列来发送消息。
-
消费者集群
消费者集群是通过部署多个消费者应用实例实现的,分两种模式:默认的集群模式和广播的集群模式。默认模式只需将消息投递到该Topic(主题)的消费者组下的一个实例即可;广播模式要求将一条消息投递到一个消费者组下的所有消费者实例。
-
-
顺序消息
指按消息的发送顺序来消费消息,RocketMQ中的顺序消息一般指局部顺序。为了满足顺序消息的要求,则必须保证按业务上顺序的把消息发送到Broker上的同一个队列中。
-
定时消息
目前RocketMQ只支持固定精度级别的定时消息,具体来说,就是按照1~N定义了如下级别:1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h。如果要发送定时消息,则在初始化消息对象之后调用setDelayTimeLevel方法设置即可。例如level为2时则会延时5s。
-
批量发送消息
如果有大量相同Topic、相同发送状态的非定时消息,则可以选择批量发送方式,批量发送消息可以提高传递消息的性能,但是一次发送的消息不能超过1M,超过了则需要分隔后再分批发送。
-
事务消息
- 使用TransactionMQProducer类创建事务消息生产者,并指定producerGroup名称,用以区别普通消息。
- 初始化时调用TransactionMQProducer对象的setExecutorService方法为本地事务执行设置合适的线程池大小。
- TransactionListener接口需要实现两个方法的返回结果都是LocalTransactionState,这是一个枚举,有三种定义,UNKNOW表示未执行完;COMMIT_MESSAGE表示执行成功,提交事务;ROLLBACK_MESSAGE表示执行失败,回滚事务。
- 用于状态回查会出现丢失,可能导致一个事务回查多次,这就需要业务回查接口能够实现幂等,RocketMQ在Message对象中提供了transactionId属性给用户用于实现初步的消息过滤,可通过getTransactionId方法获得。在实际场景中推荐根据具体业务参数做幂等判断。
-
-