目标:
从宏观上掌握RabbitMq这个消息中间件的基本原理。同时让阅读者掌握一些基本的使用方法。
大致原理介绍
为了实现解耦或者实现异步,将消息先发往独立于应用服务以外的一个中间服务(也就是mq)存储,其他服务在从这个中间服务获取消息,进行接下来的业务逻辑处理。整体流程如下:
消息中间件的作用
市场上包括各种中间件Kafka、RabbitMq、ActiveMq、RocketMq等。作用其实都是类似
- 解耦
- 流量削峰
- 异步通信
- 冗余、扩展、缓冲等
中间件的安装
RabbitMq的整体架构分析
相关名词介绍:
- producer :生产者,可以理解为发送消息的一方
- consumer:消费者,可以理解为处理消息的一方
- broker:消息中间件服务,消息的中间方。安装mq服务的节点。
消息的流转过程
其他关键名词介绍:
- 交换机:可以理解为一个路由器,整个消息进度broker的第一个处理者。根据消息的不同,将消息放入不同的队列。
- 路由键:标志消息属于哪个队列(某些情况下该参数失效)
- 绑定: 将交换机和队列进行绑定,
- 队列:整个mq服务端(发送消息和消费消息的是客户端)用于存储消息的对象;
交换机类型
其实就是介绍交换机的常见模式。有一对一,也有一对多
1、fanout:可以理解为组播
凡是绑定在交换机下的队列都能收到消息。一个交换机会绑定很多个队列,这种情况下路由键会失效。
2、direct:完全根据路由key进行路由
可以看到路由键为warning的话,消息会被推到两个队列;路由key是info的话只会进入一个队列。这个就是direct类型的交互器的特征。
3、topic:绑定的key带有通配符
路由键带有通配符
4、head
实际使用介绍
实际使用介绍之前,我们先介绍下“连接”和“信道”的概念
生产者在和mq服务通信过程中是通过TCP协议,那个二者之间就会建立tcp连接,这种连接的建立通常是非常耗费时间,所以mq的设计者就使用了复用tcp连接的思路。那channel又是什么呢?他的中文翻译是信道,这个信道我们可以理解为完成一次逻辑通信的对象。比如我们可以是整个生产者服务和mq之间只有一条TCP链路,但是生产者可以是多线程的,多线程各自维护了一个和Mq服务通信的信道,也就是这里的channl ,chanel是一条逻辑上的通信链路。Connection是一条物理上的通信链路。
那为什么不直接使用Connection呢?主要是考虑各个线程之间的数据隔离
是不是无论多少个信道都可以共用一个Connection呢?不是,当信道数量越来越多的时候,一个Tcp连接可能不够用,我们应该适当的增加物理连接的数量。
最简单使用
默认交换机的使用
# 配置类
@Configuration
//@ConditionalOnProperty(prefix = SystemProperties.PREFIX, name = "openRabbitMq", havingValue = "true", matchIfMissing = true)
public class RabbitMqConfiguration {
@Bean
CommonConsumer commonConsumer(){
return new CommonConsumer(); //申明一个默认消费者,
}
@Bean //定义一个普通队列,并没有给整个队列绑定交换机哦!
public Queue commonQueue() {
return new Queue(QueueEnum.COMMON_QUEUE.getQueueName());
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory connectionFactory, MessageConverter messageConverter) {
connectionFactory.setPublisherConfirms(true);
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
return factory;
}
@Bean
public MessageConverter messageConverter() {//申明对象序列化类
return new ContentTypeDelegatingMessageConverter(new Jackson2JsonMessageConverter());
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(messageConverter);
template.setMandatory(true);
return template;
}
}
## 生产者
package com.defire.provider;
// 实现了 InitializingBean的对象在bean初始化时会调用afterPropertiesSet方法。
//ConfirmCallback & 和ReturnCallback 是为了实现消息确认机制,保证整个中间件的高可用。我们后文还会深入探讨
@Component
public class CommonProvider implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback, InitializingBean {
static Logger logger = LoggerFactory.getLogger(CommonProvider.class);
protected RabbitTemplate rabbitTemplate;
public CommonProvider(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
/**
* 发送消息
*
* @param messageContent
*/
public void sendMessage(MessageContent messageContent, QueueEnum queueEnum) {
if (messageContent != null ) {
messageContent.setExchange(queueEnum.getExchange());//当使用默认队列的时候,交换机的名字是空
messageContent.setQueueName(queueEnum.getQueueName());
messageContent.setRouteKey(queueEnum.getRouteKey());
MyCorrelationData correlationData = new MyCorrelationData(messageContent.getMessageId(), messageContent);
correlationData.setExchange(queueEnum.getExchange());
correlationData.setRoutingKey(queueEnum.getRouteKey());
// 执行发送消息到指定队列
rabbitTemplate.convertAndSend(queueEnum.getExchange(), queueEnum.getRouteKey(), messageContent, correlationData);
logger.debug("CommonProvider新增消息内容:{}", JSON.toJSONString(messageContent));
} else {
logger.warn("消息内容为空或未开启队列!!!!!");
}
}
/**
* 用于实现消息发送到RabbitMQ交换器后接收ack回调,
* 如果消息已经到到中间件,则会回调该方法。该方法在afterPropertiesSet中已经配置到rabbitTemplate对象上。
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.debug("CommonProvider消息到达exchange成功,{}", correlationData == null ? cause : correlationData);
} else {
logger.debug("CommonProvider消息到达exchange失败,{}", correlationData == null ? cause : correlationData);
}
}
/**
* 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调。
* 如果消息已经到到中间件,但是中间件没有知道到对应的队列,则会回调该方法。
* 该方法在afterPropertiesSet中已经配置到rabbitTemplate对象上。
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
logger.error("CommonProvider发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
}
}
@Override
public void afterPropertiesSet() throws Exception {
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
}
上文简单的介绍了仅仅申明一个队列,未申明交互机,未进行绑定的默认情况。
如果使用默认交换机,则消息一定会被投递到和路由键一致的队列中。下面介绍其他类型的用法
其他特殊用法(两种实现延迟队列的方式)
所谓延迟队列,就是生产者将消息发送到队列后,队列不是立即消费,而是等待一段时间后才开始消费。常见的使用场景是交易系统中下单超时未支付的订单需要让其失效。为啥实现延时队列有两种方式呢?又是哪两种呢?
第一种是TTL队列+死信队列,第二种就是叫延迟队列。为啥有了第二种还需要第一种,我猜想是因为一开始并没有第二种,当延迟队列的需求确实很多了,官方才提供了延迟队列的插件。默认的rabbitmq是没有延迟队列的。需要为其单独安装插件。
上文刚刚提到了TTL队列,
什么是ttl呢?
其全称是time to live ,也就是队列里面的数据存在生存周期,如果生存周期内数据未被消费掉,那么,消息将被自动删除,当然如果ttl队列关联死信队列,则可以将ttl队列的消息转移至死信队列。
什么又是死信队列?
死信队列专门用于作为其他队列的协助队列,当主队列的消息不能被正常消费,或者主队列的消息过期了,则消息自动转到死信队列。
如何实现第一种延迟队列呢?设置两个队列,队列1(TTL队列)先收到生产者发来的消息,然而并没有消费者会消费队列1的消息,直到队列1消息过期,消息被转移到队列1关联的队列2(死信队列)而这个死信队列是指定了消费者的,所以消息一旦转移到死信队列,则立即被消费。如下图
代码上如何实现?
/**
* TTL交换机配置
*/
@Bean
DirectExchange ttlDirectExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
.durable(true)
.build();
}
/**
* 定义TTL队列
* 注意这个ttl队列申明的时候指定了死信交换机
*/
@Bean
Queue ttlQueue() {
return QueueBuilder
.durable(QueueEnum.MESSAGE_TTL_QUEUE.getQueueName())
// 配置到期后转发的交换
.withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())//注意这里绑定了死信交换机
// 配置到期后转发的路由键
.withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey())
//注意这里绑定了死信队列
.build();
}
// 死信交换机和死信队列
/**
* 死信消息交换机配置
*/
@Bean
DirectExchange deadDirectExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())
.durable(true)
.build();
}
/**
* 定义死信队列
*/
@Bean
public Queue deadQueue() {
return new Queue(QueueEnum.MESSAGE_DEAD_QUEUE.getQueueName());
}
/**
* 死信队列和死信交换机的绑定-routekey
* @param deadDirectExchange 消息中心交换配置
* @param deadQueue 消息中心队列
*/
@Bean
Binding messageBinding(DirectExchange deadDirectExchange, Queue deadQueue) {
return BindingBuilder
.bind(deadQueue)
.to(deadDirectExchange)
.with(QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey());
}
/**
* ttl队列和ttl交换机的绑定-routekey
* @param ttlQueue
* @param ttlDirectExchange
*/
@Bean
public Binding messageTtlBinding(Queue ttlQueue, DirectExchange ttlDirectExchange) {
return BindingBuilder
.bind(ttlQueue)
.to(ttlDirectExchange)
.with(QueueEnum.MESSAGE_TTL_QUEUE.getRouteKey());
}
上文提到的都是如何生产消息,还没有提到如何消费消息。
@RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)//指定队列名字即可,如果是延迟队列,这里应该指定死信队列的名字
public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
if (messageContent != null) {
//做业务逻辑
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//消费完后回复mq,mq则会从待确认队列中删除这个消息。如此来保证整体的可靠性。
log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
}
}
以上我们分析完了第一种延迟队列的实现,现在我们看看第二种,第二种就更简单了。
/**
* 定义延迟队列
* @return
*/
@Bean
public Queue delayQueue(){
return new Queue(QueueEnum.MESSAGE_DELAY_QUEUE.getQueueName());
}
/**
* 延迟消息交换机配置
* @return
*/
@Bean
CustomExchange delayExchange(){
Map<String,Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(QueueEnum.MESSAGE_DELAY_QUEUE.getExchange(), "x-delayed-message", true, false, args);
}
//绑定
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
return BindingBuilder.bind(delayQueue).to(delayExchange).with(QueueEnum.MESSAGE_DELAY_QUEUE.getRouteKey()).noargs();
}
至此延迟队列申明就完成,接下来的消费,就和普通队列完全一致了。同时我们对于延迟队列这种特殊队列的介绍也暂时完成,为此我们引入了TTL队列,死信队列。其实这俩队列都是可以单独使用的,并不是完全为了延迟队列而生。
作为一个中间件,我们总是要充分考虑其可用性,可靠性。那么整个过程中,是如何保证生产的消息一定会被消费呢?
其实rabbitmq提供了事务的方式和确认机制,两种方式来保证消费的可靠性。第一个由于性能太低我们就不介绍了。我们主要介绍确认机制。确认机制其实是包括好几种的,第一种是同步确认机制,第二种是异步确认机制。
首先是生产者,发送出去的消息会有一个回调通知,通知生产者消息是否被mq接收。
/**
* 用于实现消息发送到RabbitMQ交换器后接收ack回调
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.debug("CommonProvider消息到达exchange成功,{}", correlationData == null ? cause : correlationData);
} else {
logger.debug("CommonProvider消息到达exchange失败,{}", correlationData == null ? cause : correlationData);
}
}
/**
* 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调。
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
logger.error("CommonProvider发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
}
}
@Override
public void afterPropertiesSet() throws Exception {
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this); //配置回调
this.rabbitTemplate.setReturnCallback(this);//配置回调
}
现在生产者已经放心了,自己发出去的消息有保证了。那么mq和消费者之间有哪些操作呢?
@RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)
public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
if (messageContent != null) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//通知mq,消费者已经拿到消息。
log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
}
}
channel.basicAck 通知mq,消费者已经拿到消息。
当然我们上面提到的仅是一种确认机制,能辅助三方之间沟通消息的发送,接收,消费状态。要提高整个系统的高可用,还得考虑很多其他方面。比如mq本身需要满足高可用(集群方式),还有消息&队列&交换机等实例在mq中要考虑将其持久化。这里就不深入讨论。
本文涉及到的代码可以在git上查看