AMQP
AMQP,即 Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦和通讯。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用 Erlang 语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,具有很高的易用性和可用性。
AMQP协议中的几个重要概念
ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。
ConnectionFactory:ConnectionFactory为Connection的制造工厂。
Connection:Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。
Channel(信道):信道是建立在“真实的”TCP连接上的虚拟连接,在一条TCP链接上创建多少条信道是没有限制的,把他想象成光纤就是可以了。它是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。
Queue(队列)
Queue 是 RabbitMQ 的内部对象,用于存储消息。
RabbitMQ中的消息只能存储在 Queue 中。生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费,消费者可以是一个或者多个。
Message acknowledgment(ack 消息的确认)
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(ack)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…
Message durability(消息的持久化)
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。
Prefetch count(每次向消费者发送消息的总数)
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
Exchange(交换器)
Exchange生产者将消息发送到 Exchange,由 Exchange 根据一定的规则将消息路由到一个或多个 Queue 中(或者丢弃)。
Routing key(路由key)
生产者在将消息发送给 Exchange 的时候,一般会指定一个 routing key,来指定这个消息的路由规则。 Exchange 会根据 routing key 和 Exchange Type(交换器类型) 以及 Binding key 的匹配情况来决定把消息路由到哪个 Queue。RabbitMQ为routing key设定的长度限制为255 bytes。
Binding(绑定)
RabbitMQ中通过 Binding 将 Exchange 与 Queue 关联起来。
Binding key
在绑定(Binding) Exchange 与 Queue 关系的同时,一般会指定一个 binding key。
Exchange Types (交换器类型)
RabbitMQ常用的Exchange Type有 Fanout、 Direct、 Topic、 Headers 这四种。
Fanout
这种类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,这时 Routing key 不起作用。
Direct
这种类型的Exchange路由规则也很简单,它会把消息路由到那些 binding key 与 routing key完全匹配的Queue中。
Topic
这种类型的Exchange的路由规则支持 binding key 和 routing key 的模糊匹配,会把消息路由到满足条件的Queue。 binding key 中可以存在两种特殊字符 *与 #,用于做模糊匹配,其中 * 用于匹配一个单词,# 用于匹配0个或多个单词,单词以符号“.”为分隔符。
Headers
这种类型的Exchange不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
RPC
MQ本身是基于异步的消息处理,所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。
RabbitMQ中实现RPC的机制是:
1.户端发送请求(消息)时,在消息的属性中(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
2.服务器端收到消息并处理
3.服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性
客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理
SpringBoot整合RabbitMQ:
配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=password
spring.rabbitmq.virtual-host=test
spring.rabbitmq.connection-timeout=150000
#手动确认消息已经消费
spring.rabbitmq.listener.simple.acknowledge-mode=manual
Direct模式
@Configuration
public class QueueConfig {
//直连队列
public static final String QUEUE_DIRECT_NAME = "direct_test_queue";
@Bean
public Queue directQueue(){
/**
durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
exclusive 表示该消息队列是否只在当前connection生效,默认是false
auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
*/
return new Queue(QUEUE_DIRECT_NAME,true,false,false);
}
}
@Configuration
public class ExchangeConfig {
/** 消息交换机的名字
*
* durable="true" rabbitmq重启的时候不需要创建新的交换机
* direct交换器相对来说比较简单,匹配规则为:如果路由键匹配,消息就被投送到相关的队列
*
* */
//直连交换机
public static final String DIRECT_EXCHANGE = "direct_test_exchange";
@Bean
public DirectExchange directExchange(){
DirectExchange directExchange = new DirectExchange( ExchangeConfig.DIRECT_EXCHANGE,true,false);
return directExchange;
}
}
@Configuration
public class RabbitMqConfig {
/**
* 连接工厂
*/
@Autowired
private ConnectionFactory connectionFactory;
/**
* key: queue在该direct-exchange中的key值,当消息发送给direct-exchange中指定key为设置值时,
* 消息将会转发给queue参数指定的消息队列
*/
/** 直连 绑定的 路由键 */
public static final String ROUTIN_DIRECT_KEY = "queue_direct_test_key";
@Autowired
private QueueConfig queueConfig; //队列配置信息
@Autowired
private ExchangeConfig exchangeConfig; //交换机配置信息
/**
* 匹配交换机【DIRECT_EXCHANGE】进行绑定,队列【QUEUE_DIRECT_NAME】,路由键【ROUTIN_DIRECT_KEY】
*/
@Bean
public Binding binding_direct() {
return BindingBuilder.bind(queueConfig.directQueue()).to(exchangeConfig.directExchange()).with( RabbitMqConfig.ROUTIN_DIRECT_KEY );
}
}
@Slf4j
@Component
public class DirectSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String message){
rabbitTemplate.convertAndSend(
ExchangeConfig.DIRECT_EXCHANGE,
RabbitMqConfig.ROUTIN_DIRECT_KEY,
message
);
log.info("发送消息服务器的消息:"+message);
}
}
@Slf4j
@Component
public class DirectReceive {
/**
*
* @param msg
* @param channel
* @param map
* autoStartup 设置为 false 项目启动不会自动开始监听队列 需要手动开启
* id 开启此方法的标识
*/
@RabbitHandler
@RabbitListener(
bindings = @QueueBinding(value = @Queue(value = QueueConfig.QUEUE_DIRECT_NAME,durable = "true"),
exchange = @Exchange(value = ExchangeConfig.DIRECT_EXCHANGE,durable = "true"),
key = RabbitMqConfig.ROUTIN_DIRECT_KEY),
autoStartup = "false",
id= "directTestQueue")
public void recerive(String msg, Channel channel, @Headers Map<String, Object> map){
log.info("消息服务器接收消息:"+msg);
if (map.get("error")!= null){
log.info("错误的消息");
try {
//否认消息 消息重回队列
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);
//拒绝消息 消息会被丢弃,不会重回队列
//channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);
return;
} catch (IOException e) {
e.printStackTrace();
}
}
try {
//确认消息
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@RequestMapping("/api/mquser")
@RestController
@Api(tags = "mquser")
public class MqController {
@Autowired
private RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
piOperation("打开消息服务器directTestQueue队列的监听")
@GetMapping("/openDirectTestQueue")
public Result openDirectTestQueue(){
MessageListenerContainer container = rabbitListenerEndpointRegistry.getListenerContainer("directTestQueue");
if(!container.isRunning()){
container.start();
}
return ResultUtil.success();
}
@ApiOperation("关闭消息服务器directTestQueue队列的监听")
@GetMapping("/stopDirectTestQueue")
public Result stopDirectTestQueue(){
MessageListenerContainer container = rabbitListenerEndpointRegistry.getListenerContainer("directTestQueue");
if(container.isRunning()){
container.stop();
}
return ResultUtil.success();
}
}