RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。RabbitMQ主要是用来实现应用程序的异步和解耦,同时也能起到消息缓冲,消息分发的作用。当生产者大量产生数据时,消费者无法快速消费,那么需要一个中间层。保存这个数据。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
一般的消息中间件的工作流程可以用消费者模式来表示。即消费者不断向消息队列发送消息,生产者不断从消息队列中取出消息进行消费,如下图所示:
对于rabbitMQ来说,除了最基本的消费者,消息队列,生产者之外,还引进了一个新的模块,即交换机(Exchange)。它使得生产者和消息队列之间进行了隔绝,生产者只需要将消息发给交换机,有交换机根据一定的调度策略,将消息发送给相应的消息队列。RabbitMQ的 工作流程如下:
至此,RabbitMQ的几个重要概念就出来了:
1.生产者:生产消息的应用程序
2.消费者:消费消息的应用程序
3.消息:一串二进制数据流
4.队列:消息的暂存/存储区
5.交换机:消息的中转站,用于接收分发消息, 有 fanout,direct,topic,headers四种类型
6.路由(绑定关系):将消息队列和交换机进行绑定(BindingKey),交换机才能将消息发送到指定队列。
Direct是RabbitMQ默认的交换机模式,也是最简单的模式.即创建消息队列的时候,指定一个BindingKey.当发送者发送消息的时候,指定对应的Key.当Key和消息队列的BindingKey一致的时候,消息将会被发送到该消息队列中.
topic转发信息主要是依据通配符,队列和交换机的绑定主要是依据一种模式(通配符+字符串),而当发送消息的时候,只有指定的Key和该模式相匹配的时候,消息才会被发送到该消息队列中.
Fanout是路由广播的形式,将会把消息发给绑定它的全部队列,即便设置了key,也会被忽略.
headers 也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型. 在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列.
至此,概念部分基本就完了。注意本地安装RabbitMQ的话,需要先安装Erlang,注意Erlang和rabbitMQ的版本不要差太多,否则rabbitMQ启动会报错(我安装的是RabbitMQ 3.6.5, Erlang 18.1)。
org.springframework.boot
spring-boot-starter-amqp
spring.application.name=spirng-boot-rabbitmq-sender
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#并发消费者的初始化值
spring.rabbitmq.listener.concurrency=1
#并发消费者的最大值
spring.rabbitmq.listener.max-concurrency=1
#每个消费者每次监听时可拉取处理的消息数量
spring.rabbitmq.listener.prefetch=1
下面的配置,如果不是业务必须,都可以不配置,spring-amqp中都有默认配置
这里的ConnectionFactory指的是spring-rabbit包下面的ConnectionFactory接口
@Configuration
public class MqProducerConfig {
@Autowired
@Bean
public ConnectionFactory amqpConnectionFactory(ConnectionListener connectionListener,
RecoveryListener recoveryListener,
ChannelListener channelListener) {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses("localhost:5672");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
connectionFactory.setChannelCacheSize(25);
connectionFactory.setChannelCheckoutTimeout(0);
connectionFactory.setPublisherReturns(false);
connectionFactory.setPublisherConfirms(false);
return connectionFactory;
}
}
这里主要是配置一些连接信息,都比较容易理解,下面几个可能稍微解释一下:
setCacheMode:设置缓存模式,共有两种,CHANNEL和CONNECTION模式。
CHANNEL模式,程序运行期间ConnectionFactory会维护着一个Connection,所有的操作都会使用这个Connection,但一个Connection中可以有多个Channel,操作rabbitmq之前都必须先获取到一个Channel,否则就会阻塞(可以通过setChannelCheckoutTimeout()设置等待时间),这些Channel会被缓存(缓存的数量可以通过setChannelCacheSize()设置);
CONNECTION模式,这个模式下允许创建多个Connection,会缓存一定数量的Connection,每个Connection中同样会缓存一些Channel,除了可以有多个Connection,其它都跟CHANNEL模式一样
setPublisherReturns、setPublisherConfirms:producer端的消息确认机制(confirm和return),设为true后开启相应的机制,后文详述(3.1)
这里主要是生产者发送消息时的一些配置信息。
@Bean
public RabbitTemplate rabbitTemplate() {
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean b, @Nullable String s) {
if (b){
logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,b,s);
} else {
logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,b,s);
}
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
logger.error("交换机无法将信息发送至队列:exchange({}),route({}),replyCode({}),replyText({}),message:{}",s1,s2,i,s,message);
}
});
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setRetryTemplate(retryTemplate());
return rabbitTemplate;
}
这里,主要设置了发送消息时的转换机制(setMessageConverter),以及消息是否顺利到达队列的反馈机制。
setReturnCallback、setConfirmCallback:return和confirm机制的回调接口,后文详述(3.1)。
setMandatory:设为true使ReturnCallback生效。
setRetryTemplate:重试机制,后文详述(3.2)
这个类,主要是消费者接受信息时需要注意的一些点,消费者使用@RabbitListener注解接收信息,每一个注解的方法都会由这个RabbitListenerContainerFactory创建一个MessageListenerContainer,负责接收消息。
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory,connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConcurrentConsumers(environment.getProperty("spring.rabbitmq.listener.concurrency",int.class));
factory.setMaxConcurrentConsumers(environment.getProperty("spring.rabbitmq.listener.max-concurrency",int.class));
factory.setPrefetchCount(environment.getProperty("spring.rabbitmq.listener.prefetch",int.class));
factory.setDefaultRequeueRejected(true);
factory.setErrorHandler(new ErrorHandler() {
@Override
public void handleError(Throwable throwable) {
logger.error("接收消息异常:",throwable);
}
});
return factory;
}
factoryConfigurer.configure:设置spring-amqp的ConnectionFactory。
setMessageConverter:设置消费者端的消息转换器。
setAcknowledgeMode:设置consumer端的应答模式,共有三种:NONE、AUTO、MANUAL。
NONE,无应答,这种模式下rabbitmq默认consumer能正确处理所有发出的消息,所以不管消息有没有被consumer收到,有没有正确处理都不会恢复;
AUTO,由Container自动应答,正确处理发出ack信息,处理失败发出nack信息,rabbitmq发出消息后将会等待consumer端的应答,只有收到ack确认信息才会把消息清除掉,收到nack信息的处理办法由setDefaultRequeueRejected()方法设置,所以在这种模式下,发生错误的消息是可以恢复的。
MANUAL,基本同AUTO模式,区别是需要人为调用方法给应答。
setConcurrentConsumers:设置每个MessageListenerContainer将会创建的Consumer的最小数量,默认是1个。
setMaxConcurrentConsumers:设置每个MessageListenerContainer将会创建的Consumer的最大数量,默认等于最小数量。
setPrefetchCount:设置每次请求发送给每个Consumer的消息数量。
setDefaultRequeueRejected:设置当rabbitmq收到nack/reject确认信息时的处理方式,设为true,扔回queue头部,设为false,丢弃。
setErrorHandler:实现ErrorHandler接口设置进去,所有未catch的异常都会由ErrorHandler处理。
至此,一些准备配置基本都已经配置完毕,剩下的就是按照Rabbit MQ的工作流程,配置生产者,消费者,交换机,队列以及绑定关系。
@Bean
public Queue firstQueue(){
return new Queue("first-queue",true);
}
这里只是简单创建一个Queue。
name:队列名称
durable:持久化 rabbitmq重启的时候不需要创建新的队列)。
@Bean
public DirectExchange directExchange(){
DirectExchange exchange = new DirectExchange("directExchange1",true, false);
return exchange;
}
这里只配置了最简单的Direct类型的交换机,参数分别为:
name:交换机名字
durable:持久化 rabbitmq重启的时候不需要创建新的交换机
auto-delete:表示交换机没有在使用时将被自动删除 默认是false
@Bean
public Binding bindingOne(){
return BindingBuilder.bind(queueConfig.firstQueue()).to(exchangeConfig.directExchange()).with("test111");
}
BindingBuilder.bind(队列).to(交换机).with(路由键(routing-key))。
@Component
public class Sender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("directExchange1","test111",msg,correlationData);
}
}
可以看到,生产者利用rabbitTemplate发送消息,参数分为为:
exchange:交换机名称
routingKey:路由键
msg:发送的消息
CorrelationData:消息的一个唯一标识,当消息发送失败时,可以根据这个标识找回消息做后续处理(见2.4 的confirmCallBack)
@Component
public class Receiver {
@RabbitListener(queues = "first-queue", containerFactory = "multiListenerContainer")
public void handleMessage(Message msg, Channel channel) throws Exception{
String o =new String(msg.getBody(),"UTF-8");
Book book = JSONObject.parseObject(o, Book.class);
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
System.out.println("接受到消息:"+ book.getAuthor());
}
}
@RabbitListener:监听器,两个参数分布为监听的队列(需要注意的是,该队列必须已经有了绑定关系,否则会报错),以及RabbitListenerContainerFactory(见2.5)。当队列中有消息时,会触发该注解修饰的方法。具体实现过程见:@RabbitListener实现过程
方法中有两个参数,一个是Message,一个是Channel。
Message:Spring AMQP 对消息的封装,其 byte[] body 属性代表消息内容,MessageProperties messageProperties 属性代表消息属性。
Channel:RabbitMQ的连接通道。
当设置消费者手工确认消息时(见2.5 setAcknowledgeMode),消费者可以根据自己实际的业务需求确定是否成功消费信息,并且利用Channel的方法反馈回去。
注意2.5 setDefaultRequeueRejected 方法,设置了消息拒绝后,要不丢弃,要不扔回队列头。扔回队列头时,下一次消费还会是这条消息,所以这边可以先确认消息,再把消息发送到队列,这样就到队列尾了。
至此,配置的部分已经全部讲完,一个简单的消息队列也已经建立完成!
现在有一个问题,生产者向消息队列发送完消息后,它怎么知道消息有没有顺利的到达消息队列里面。如果没有到达,问题出现在哪里,怎么补救?
前面已经说过,生产者发送消息时,是通过rabbitTemplate来发送的。我们在配置rabbitTemplate的Bean的时候,可以指定两个callback:ReturnCallback和ConfirmCallBack。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
//TODO SOMETHING
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//TODO SOMETHING
}
});
ConfirmCallback:每一条发出的消息都会调用ConfirmCallback;
ReturnCallback:只有在消息进入exchange但没有进入queue时才会调用。
如果消息没有到exchange,则confirm回调,ack=false
如果消息到达exchange,则confirm回调,ack=true
exchange到queue成功,则不回调return
exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
现在,我们已经知道了生产者发送消息何时反馈给我们,但是出问题后我们还没有补救措施。returnCallBack的回调信息中有很明显的Message以及交换机、路由等信息,我们可以直接再次尝试发送,但是confirmCallBack没有这些信息,我们该怎么处理呢?
通过观察我们发现,RabbitTemplate的convertAndSend方法中,有参数CorrelationData ,ConfirmCallBack中也有该参数,通过测试发现,对于同一条消息,它们是一致的。这下问题就有了解决方法了!
rabbitTemplate.convertAndSend(String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData) throws AmqpException;
在发送信息的时候,我们可以自定义一个消息类,将发送的消息的信息(消息体,交换机,路由等)和CorrelationData绑定到一起。再继承一个ConcurrentHashMap 将关联关系保存到内存中。
public class MessageWithId {
private String id;
private String exchangeName;
private String rountingKey;
private Object msg;
}
@Component
public class SendMessageMap extends ConcurrentHashMap{}
发送消息的时候,同时将关联关系保存一下。
@Component
public class Sender {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private SendMessageMap hashMap;
public void send(Object msg){
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
MessageWithId message = new MessageWithId();
message.setExchangeName("directExchange1");
message.setId(correlationData.getId());
message.setMsg(msg);
message.setRountingKey("test111");
hashMap.put(correlationData.getId(),message);
rabbitTemplate.convertAndSend("directExchange1","test111",msg,correlationData);
}
}
ConfirmCallBack再根据回掉信息,以及关联关系,对消息进行相应处理。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
if (ack){
logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,b,s);
//成功则从内存中移除关联关系
hashMap.remove(correlationData.getId());
logger.error("消息移除成功!");
} else {
logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,b,s);
//失败则设置一个最大重试次数重新发送!
rabbitTemplate.convertAndSend();
}
}
});
现在看起来好像是没什么问题了,但是直接在callBack中使用rabbitTemplate,会导致死锁!
解决方案也很简单,将CallBack剥离出来就行了,内部通过构造函数将rabbitTemplate引进来。
public class ConfirmCallBack implements RabbitTemplate.ConfirmCallback {
private Logger logger = LoggerFactory.getLogger(ConfirmCallBack.class);
private RabbitTemplate rabbitTemplate;
private SendMessageMap hashMap;
public ConfirmCallBack(RabbitTemplate rabbitTemplate, SendMessageMap hashMap) {
this.hashMap = hashMap;
this.rabbitTemplate = rabbitTemplate;
}
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
try {
MessageWithId message = (MessageWithId) hashMap.get(correlationData.getId());
if (ack){
logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,ack,s);
hashMap.remove(correlationData.getId());
} else {
logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,ack,s);
rabbitTemplate.convertAndSend("directExchange1",message.getRountingKey(),message.getMsg(),new CorrelationData(message.getId()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后rabbiteTemplate传进去参数。
rabbitTemplate.setConfirmCallback(new ConfirmCallBack(rabbitTemplate, hashMap));
将交换机名称故意写错,试一下能否成功。
2019-05-16 15:57:06.796 ERROR 8868 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'directExchange2' in vhost '/', class-id=60, method-id=40)
2019-05-16 15:57:09.684 ERROR 8868 --- [nectionFactory1] c.r.sender.callBack.ConfirmCallBack : 消息发送至交换机失败:correlationData(CorrelationData [id=1a63d944-d64d-4757-be91-88d2e9aea7d1]),ack(false),cause(channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'directExchange2' in vhost '/', class-id=60, method-id=40))
2019-05-16 15:57:13.107 ERROR 8868 --- [ 127.0.0.1:5672] c.r.sender.callBack.ConfirmCallBack : 消息发送至交换机成功:correlationData(CorrelationData [id=1a63d944-d64d-4757-be91-88d2e9aea7d1]),ack(true),cause(null)
可以看到,大功告成!其实这里只是演示了一个最简单的处理方案。其他方案比如,设置最大重试次数,依然失败记录信息,清除内存中的数据。或者将失败的信息保存起来,利用调度重新发送等等。
另外,这里主要写了ConfirmCallBack,另一个ReturnCallBack也是一样的处理方案。
在2.4将配置rabbitTemplate的时候,加入了重试机制,重试机制针对的是网络不稳定导致的连接中断的问题,而不是消息的重发。
rabbitTemplate.setRetryTemplate(retryTemplate());
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(Integer.MAX_VALUE);
retryTemplate.setRetryPolicy(simpleRetryPolicy);
return retryTemplate;
}
连接断开的时候,生产者消息发不出去,就会触发3.1我们讲的东西,这个时候,我们可以把发送失败的消息存起来,利用调度去定时跑这些任务。
首先,对rabbitMQ消费过程不是很清楚的话,可以参考spring-rabbit消费过程解析 以及 @RabbitListener实现过程
在2.5我们讲到,消费者端对消息有三种应答方式,这里我们选择人工处理(AcknowledgeMode.MANUAL)。原因是可以防止rabbitMQ消息丢失。
MANUAL模式下,Broker对消费者拿走的消息,会处于unack(未应答)模式,只有当消费者端手工应答ack的时候,才会从消息队列里面删除。
如果由于程序异常,或者消费者自己拒绝应答(basicNack)消息,该消息会按照我们2.5配置的策略扔回队列头部或者丢弃。
针对多个消费者消费同一个队列时(多个@RabbitListener),被丢弃的消息可能会被其他消费者捕获到。
针对单一消费者,扔回头部的消息又会被自己捕捉到,从而照成死循环。
解决方案有:
1.设置最大重试次数,超过后记录详细信息,返回消费成功让消息队列删除该消息。
2.引入类似死信队列,将nack的消息放入该队列,不影响正常队列消费。
DLX, Dead-Letter-Exchange。
将一个queue设置了x-dead-letter-exchange及x-dead-letter-routing-key两个参数后,这个queue里丢弃的消息将会进入dead letter exchange,并route到相应的queue里去。
利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一向有一下几种情况:
消息被拒绝(basic.reject/ basic.nack)并且requeue=false
消息TTL过期
队列达到最大长度
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列,可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ 3.0以前支持的immediate参数(可以参考RabbitMQ之mandatory和immediate)的功能。
作者也是第一次在实际项目中使用rabbitMQ,可能有很多地方表述不清晰,或者有误,希望大家指正!
SpringBoot整合RabbitMQ之 典型应用场景实战一
springboot学习笔记-6 springboot整合RabbitMQ
Springboot整合一之Springboot整合RabbitMQ
Spring整合rabbitmq实践(一):基础使用配置
Spring整合rabbitmq实践(二):扩展功能