消息中间件
消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。
组件组成
- Broker(消息服务器)
作为server提供消息核心服务
- Producer(消息生产者)
业务的发起方,负责生产消息传输给broker
- Consumer(消息消费者)
业务的处理方,负责从broker获取消息并进行业务逻辑处理
- Topic(主题)
发布订阅模式下的消息统一汇集地,不同生产者向topic发送消息,由MQ服务器分发到不同的订阅者,实现消息的广播
- Queue(队列)
PTP模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收
- Message(消息体)
根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输
常见模式
-
PTP点对点
使用queue作为通信载体,消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。 Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。 -
Pub/Sub(发布/订阅)
即广播模式,使用topic作为通信载体。消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。 -
区别
Queue(队列)实现了负载均衡,将producer生产的消息发送到消息队列中,由多个消费者消费。但一个消息只能被一个消费者接受,当没有消费者可用时,这个消息会被保存直到有一个可用的消费者。
Topic(主题)实现了发布和订阅(广播模式),当你发布一个消息,所有订阅这个topic的服务都能得到这个消息,所以从1到N个订阅者都能得到一个消息的拷贝。
消息中间件常用协议
- AMQP协议
AMQP即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。
优点:可靠、通用
- MQTT协议
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统
- STOMP协议
STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。
优点:命令模式(非topic\queue模式)
- XMPP协议
XMPP(Extensible Messaging and Presence Protocol,可扩展消息处理现场协议)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。
优点:通用公开、兼容性强、可扩展、安全性高
缺点:XML编码格式占用带宽大
- 其他基于TCP/IP自定义的协议
有些特殊框架(如:redis、kafka、zeroMq等)根据自身需要未严格遵循MQ规范,而是基于TCP\IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。
消息中间件的优势
- 系统解耦
交互系统之间没有直接的调用关系,只是通过消息传输,故系统侵入性不强,耦合度低。
- 提高系统响应时间
例如原来的一套逻辑,完成支付可能涉及先修改订单状态、计算会员积分、通知物流配送几个逻辑才能完成;通过MQ架构设计,就可将紧急重要(需要立刻响应)的业务放到该调用方法中,响应要求不高的使用消息队列,放到MQ队列中,供消费者处理。
- 为大数据处理架构提供服务
通过消息作为整合,大数据的背景下,消息队列还与实时处理架构整合,为数据处理提供性能支持。
- Java消息服务——JMS
Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
JMS中的P2P和Pub/Sub消息模式:点对点(point to point, queue)与发布订阅(publish/subscribe,topic)最初是由JMS定义的。这两种模式主要区别或解决的问题就是发送到队列的消息能否重复消费(多订阅)。
消息中间件的应用场景
异步处理
场景:用户注册后,需要发注册邮件和注册短信
- 解决方案1:串行方式,将注册信息写入数据库后(50ms),发送注册邮件(50ms),再发送注册短信(50ms),以上三个任务全部完成后(150ms)才返回给客户端。
- 然而,邮件和短信并不是必须的,只是一个通知,这种做法存在大量的时间开销(150ms)。
- 解决方案2:并行方式,将注册信息写入数据库后(50ms),同时发送邮件和短信(50ms),以上三个任务完成后(100ms)返回给客户端,并行方式相比于串行方式能提高处理的时间。
- 然而,邮件和短信并不是必须的这个条件依旧存在,客户端还是需要等待(100ms),服务器需要邮件和短信发送完成才返回响应。
- 解决方案3:消息队列方式,把发送邮件和短信这两个不是必须的业务逻辑异步处理放入消息队列,不进行立即处理,等服务器空闲时(即有多余的消费者时)再进行处理。这种方式使服务器的响应时间实际上就是注册信息写入数据库的时间(50ms),因为写入消息队列的时间开销可忽略不计,大大提高了服务器响应速率。
应用解耦
场景:电商网站中,用户下单后,订单系统需要通知库存系统,进行库存验证与修改。
- 解决方案1:传统的做法,在下单后,订单系统立即调用库存系统的接口进行查询与修改操作。
- 然而,当库存系统出现故障时,订单系统的操作无法完成,就会报错和失败。因为传统操作导致订单系统和库存系统高度耦合,一环出错全盘出错。
- 解决方案2:消息队列实现,用户下单后,订单系统完成持久化处理(将订单写入数据库),将消息(对库存系统的操作)写入消息队列后,返回用户订单下单成功。库存系统陆续订阅下单的消息,获取下单消息,进行库操作。通过消息队列对下单操作进行解耦,及时库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。
流量削峰
场景:秒杀活动中一般会因为突然的流量过大,导致应用挂掉。
解决方案:加入消息队列来处理。通过控制活动人数,超过此一定阀值的订单直接丢弃(也就是秒杀失败),缓解短时间的高流量压垮应用。实现的原理就是,用户发送请求,服务器收到之后,写入消息队列,当消息队列长度超过最大值,直接抛弃用户请求或跳转到错误页面。等有消费者空闲时,根据消息队列中的请求信息,再陆续处理。
这里以RabbitMQ为例子
使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB整合。
AMQP(Advanced Message Queue Protocol 高级消息队列协议):是一个网络协议,它支持符合条件的客户端和消息代理中间件(message middleware broker)进行通讯。
RabbitMQ是AMQP协议的实现者,所以AMQP中的概念和准则也适用于RabbitMQ。
AMQP模型组件
- Broker:是接收和分发消息的应用,提供一种传输服务,维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输,。
- Exchange:消息交换机,指定消息按什么规则,路由到哪个Queue,Message消息先要到达Exchange。Exchange在Server中承担着从Produce接收Message的责任。
- Queue:消息的载体,每个消息都会被投到一个或多个队列, 是Message的容器,等待被消费出去。
- Binding:绑定,Exchange就是根据这些定义好的Routing key将Message送到对应的Queue中去,是Exchange和Queue之间的桥梁。
- Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
- VirtualHost:虚拟主机,一个broker里可以有多个VirtualHost,用作不同用户的权限分离。
- Producer:消息生产者,就是投递消息的程序。
- Consumer:消息消费者,就是接受消息的程序。
- Connection:是Publisher/Consumer和Broker之间的TCP连接。断开连接的操作只会Publisher/Consumer端进行,Broker不会断开连接,除非出现网络故障或者Broker服务出现问题,Broker服务宕了。
- Channel:消息通道,在客户端的每个连接里,可建立多个channel。
交换机(Exchange)
直接交换(Direct Exchange):消息分发到特定路由关键字的队列。
- 直连交换机(direct exchange):将交换机和一个队列绑定起来,消息分发到这个队列。直连交换机经常用来循环分发任务给多个消费者,此时消息的负载均衡是发生在消费者之间的。
- 主题交换机(topic exchange):将交换机和队列绑定起来,通过匹配规则的路由关键字匹配,将消息分发到一个或多个消息队列。经常用来实现各种分发、订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。
- 扇形交换机/广播交换机(fanout exchange):将交换机和任意多个队列绑定起来,消息分发到所有绑定的队列。
- 头交换机(header exchange):有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机使用多个消息属性来代替路由键建立路由规则。通过消息头部属性匹配,消息分发到一个或多个消息队列。这种方式比较灵活,但每个消息会有额外的匹配属性。
- 默认交换机(default exchange):默认交换机是一种特殊的直连交换机(direct exchange)。它是由消息代理默认声明的,该交换机有一个特性,所有新建的队列都会默认绑定到默认交换机上,并且绑定的routing-key就是队列的名字。
交换机的属性有以下几个
- Name:交换机名称。
- Durable:消息代理重启后,交换机是否还存在。交换机有两个状态,持久(durable)、暂存(transient)。持久化的交换机会在消息代理重启后依旧存在,而暂存的交换机则不会。
- Auto-delete :当所有与之绑定的消息队列都完成了对此交换机的使用后,删掉它。
- Arguments:依赖代理本身。
RabbitMQ常见模式
可参考博文rabbitmq系列(二)几种常见模式的应用场景及实现
Spring整合RabbitMQ
- 添加依赖
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>3.2.8.RELEASE</version>
</dependency>
- 进行spring-rabbitmq.xml的配置
下面涉及到的host,那台机子上需要安装RabbitMQ,可参考博文CentOS7安装RabbitMQ
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit-1.2.xsd">
<!-- RabbitMQ公共配置部分 start -->
<!--配置connection-factory,指定连接rabbit server参数 -->
<rabbit:connection-factory id="connectionFactory" virtual-host="/" username="guest" password="guest" host="***.***.***.***" port="5672" />
<!--通过指定下面的admin信息,当前producer中的exchange和queue会在rabbitmq服务器上自动生成 -->
<rabbit:admin id="connectAdmin" connection-factory="connectionFactory" />
<!-- 定义 topic方式的exchange、队列、消息收发 start -->
<!--定义queue -->
<!--其中durable是是否持久划的标志,默认是true
exclusive 仅创建者可以使用的私有队列,断开后自动删除
auto-delete 当所有消费端连接断开后,是否自动删除队列 -->
<rabbit:queue name="topic_queue" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />
<!-- 交换机定义 -->
<!--
交换机:一个交换机可以绑定多个队列,一个队列也可以绑定到多个交换机上。
如果没有队列绑定到交换机上,则发送到该交换机上的信息则会丢失。
direct模式:消息与一个特定的路由器完全匹配,才会转发
topic模式:按模式匹配
-->
<!--这里绑定direct_queue -->
<rabbit:topic-exchange name="exchange_topic" durable="true" auto-delete="false">
<rabbit:bindings>
<!-- 设置消息Queue匹配的pattern (direct模式为key) -->
<rabbit:binding queue="topic_queue" pattern="notice.*" />
</rabbit:bindings>
</rabbit:topic-exchange>
<!--<rabbit:direct-exchange name="exchange_direct" durable="true" auto-delete="false">-->
<!--<rabbit:bindings>-->
<!--key:绑定的路由键,需要在交换器中绑定。-->
<!--<rabbit:binding queue="test_queue" key="msg.test" />-->
<!--</rabbit:bindings>-->
<!--</rabbit:direct-exchange>-->
<!--定义rabbit template用于数据的接收和发送 -->
<rabbit:template id="topicAmqpTemplate" connection-factory="connectionFactory" exchange="exchange_topic" />
<!-- 以下是消息接收者配置 -->
<!--<bean id="topicMessageReceiver" class="com.***.TopicMessageReceiver"></bean>-->
<!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
<!-- <rabbit:listener-container
connection-factory="connectionFactory" acknowledge="auto">
<rabbit:listener queues="topic_queue_t" ref="topicMessageReceiver" />
</rabbit:listener-container>-->
</beans>
- 导入到applicationContext.xml中
<import resource="classpath*:/spring/spring-rabbitMQ.xml" />
- 生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
@Service
public class TopicMessageProducer {
@Resource(name = "topicAmqpTemplate")
private AmqpTemplate topicAmqpTemplate;
@Async
public void sendMessage(Object message) throws IOException {
//未持久化的消息
topicAmqpTemplate.convertAndSend("notice.info", message);
//rabbitMQ的消息持久化
ConnectionFactory factory=new ConnectionFactory(); //创建连接工厂
factory.setHost("***.***.***.***");//IP
Connection connection=factory.newConnection(); //创建连接
Channel channel=connection.createChannel();//创建信道
//将队列设置为持久化之后,还需要将消息也设为可持久化的,MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublish("exchange_topic","notice.info", MessageProperties.PERSISTENT_TEXT_PLAIN,message.toString().getBytes());
System.out.println("持久化结束");
}
}
- 消费者
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class TopicReceiver {
@RabbitListener(queues ="topic_queue" )
public void receiveMessage1(String str){
System.out.println("我是消费者----------- , "+str);
}
}
基本的RabbitMQ发布订阅就实现了
参考博文:
【1】:RabbitMQ的使用
【2】:RabbitMQ应用场景与实例