1.导入依赖
<!-- 导入Spring AMQP依赖,包含rabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.application.yml配置
spring:
rabbitmq:
#主机地址
host: 127.0.0.1
#端口号
port: 5672
#虚拟主机
virtual-host: /demo
#用户名
username: guest
#密码
password: guest
#消费者需要添加该监听器配置
listener:
simple:
#每次只能获取一条消息,处理完成才能获取下一条消息
prefetch: 1
3.消息收发示例
消息发送者需要注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
注解式声明队列和交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="队列名称"),
exchange = @Exchange(name="交换器名称",type = ExchangeTypes.TOPIC),
key = {"Routing key"}
))
(1)fanout模式
fanout交换机会将接收到的消息广播到每一个跟其绑定的队列,所以也叫广播模式
消息发送者
@Test
void testFanoutExchange() {
//使用rabbitMQ的fanout交换机往队列中发送消息
//交换机需要绑定指定的队列
//交换机名称
String exchangeName = "demo_fanout";
//消息内容
String message = "Hello World!";
rabbitTemplate.convertAndSend(exchangeName,null, message);
}
消息接收者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="fanout_queue1"),
exchange = @Exchange(name="demo_fanout",type = ExchangeTypes.FANOUT)
))
public void getFanoutQueueMessage1(String message) {
System.out.println(message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="fanout_queue2"),
exchange = @Exchange(name="demo_fanout",type = ExchangeTypes.FANOUT)
))
public void getFanoutQueueMessage2(String message) {
System.err.println(message);
}
接收到的消息
(2)direct模式
direct交换机会将接收到的消息根据规则路由到指定的队列,因此成为定向路由。
- 每一个队列都要与交换器设置一个Binding Key;
- 发布者发送消息时,需要指定消息的Routing Key;
- 交换器会将消息路由到BindingKey与消息Routing Key一直的队列。
消息发送者
@Test
void testDirectExchange() {
//使用rabbitMQ的direct交换机往队列中发送消息
//交换机需要绑定指定的队列,并设置相应的routing key
//交换机名称
String exchangeName = "demo_direct";
//消息内容
String message1 = "red message";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"red", message1);
//消息内容
String message2 = "blue message";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"blue", message2);
}
消息接收者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="direct_queue1"),
exchange = @Exchange(name="demo_direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void getDirectQueueMessage1(String message) {
System.out.println("direct_queue1====>>"+message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="direct_queue2"),
exchange = @Exchange(name="demo_direct",type = ExchangeTypes.DIRECT),
key = {"red"}
))
public void getDirectQueueMessage2(String message) {
System.err.println("direct_queue2====>>"+message);
}
接收到的消息
(2)topic模式
topic交换器与direct交换器类似,区别在于RoutingKey可以是多个单词的列表,并且以.
分割
队列与交换器指定BindingKey时可以使用通配符:
#
:代指0个或多个单词
*
:代指一个单词
消息发送者
@Test
void testTopicExchange() {
//使用rabbitMQ的topic交换机往队列中发送消息
//交换机需要绑定指定的队列,并设置相应的routing key,可以是多个单词列表,并以.分割,#代指0个或多个单词,*代指一个单词
//交换机名称
String exchangeName = "demo_topic";
//消息内容
String message1 = "china's news";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"china.news", message1);
//消息内容
String message2 = "japan's news";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"japan.news", message2);
//消息内容
String message3 = "china's weather";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"china.weather", message3);
}
消息接收者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="topic_queue1"),
exchange = @Exchange(name="demo_topic",type = ExchangeTypes.TOPIC),
key = {"china.#"}
))
public void getTopicQueueMessage1(String message) {
System.out.println("topic_queue1====>>"+message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="topic_queue2"),
exchange = @Exchange(name="demo_topic",type = ExchangeTypes.TOPIC),
key = {"#.news"}
))
public void getTopicQueueMessage2(String message) {
System.err.println("topic_queue2====>>"+message);
}
接收到的消息
4.发送和接收Object类型数据
Spring amqp对消息对象的处理是基于JDK的序列化完成的,其存在安全风险大、消息存储空间太大、可读性差等问题,建议采用JSON序列化来代替默认的JDK序列化。
(1)导入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
(2)添加Configuration
@Configuration
public class MqConfig {
//采用JSON序列化代替Spring amqp默认的JDK序列化
@Bean
public MessageConverter jacksonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
注:上述两步在生产者和消费者中都要添加
(3)消息发送者
@Test
void testSendObject() {
//往队列中发送消息Object类型消息
String exchangeName = "object_exchange";
//消息内容
JSONObject message = new JSONObject();
message.put("name","jack");
message.put("age",20);
rabbitTemplate.convertAndSend(exchangeName,null, message);
}
(4)消息接收者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="object_queue",durable = "true"),
exchange = @Exchange(name="object_exchange",type = ExchangeTypes.FANOUT)
))
public void getObjectMessage(JSONObject message) {
System.out.println(message);
}
(5)接收到的消息
5.生产者可靠性
(1)生产者重连机制
有的时候由于网络波动,可能会出现客户端连接MQ失败的情况,可以通过配置开启链接失败后的重连机制。
application.yml配置:
spring:
rabbitmq:
#生产者重连机制
#MQ的连接超时时间
connection-timeout: 1s
template:
retry:
#开启超时重试机制
enabled: true
#失败后的初始等待时间
initial-interval: 1000ms
#失败后下次的等待时长倍数
multiplier: 1
#最大重试次数
max-attempts: 3
注:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,需要合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
(2)生产者确认机制
RabbitMQ提供了Publisher Confirm和Publisher Return两种确认机制,开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。消息投递成功会返回ACK,投递失败会返回NACK。
application.yml配置:
spring:
rabbitmq:
#生产者确认机制
#开启生产者确认机制,并设置confirm类型,none:关闭confirm机制,simple:同步阻塞等待MQ的回执消息,correlated:MQ异步回调方式返回回执消息
publisher-confirm-type: correlated
#开启生产者回调机制
publisher-returns: true
Publisher Return机制
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
//设置ReturnCallback
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.debug("收到消息的return callback,exchange:{},routingKey:{},msg:{},code:{}.text:{}",
returnedMessage.getExchange(), returnedMessage.getRoutingKey(),returnedMessage.getMessage(),
returnedMessage.getReplyCode(),returnedMessage.getReplyText());
}
});
}
}
Publisher Confirm机制
@Test
void textConfirmCallback() throws InterruptedException {
//1.创建CorrelationData
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
//2.添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onSuccess(CorrelationData.Confirm result) {
System.out.println("收到confirm callback回执");
log.debug("收到confirm callback回执");
if (result.isAck()){
log.debug("消息发送成功,收到ack");
}else{
log.error("消息发送失败,收到nack,原因:{}",result.getReason());
//这里可以进行消息重发
}
}
@Override
public void onFailure(Throwable ex) {
log.error("消息回调失败",ex);
}
});
//交换机名称
String exchangeName = "demo_direct";
//消息内容
String message1 = "red message";
//根据路由键routing key的值进行分发
rabbitTemplate.convertAndSend(exchangeName,"red", message1,cd);
Thread.sleep(2000);
}
注:
- 生产者确认需要额外的网络和系统资源开销,尽量不要使用
- 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
- 对于nack消息可以有限次数重试,依然失败则记录异常消息
(3)数据持久化
将消息数据存储在磁盘中以防止数据丢失,实现数据的持久化,可以设置durable = "true"
开启数据持久化,Spring amqp默认队列就是持久化的。
Lazy Queue
从RabbitMQ 3.6版本开始,增加了Lazy Queue概念,也称惰性队列。
Lazy Queue的特征:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持百万条的消息内存
在3.12版本后,默认队列就是Lazy Queue,无法更改
//设置队列为lazy queue
//arguments = @Argument(name="x-queue-mode",value = "lazy")),
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="lazy_queue",
arguments = @Argument(name="x-queue-mode",value = "lazy")),
exchange = @Exchange(name="lazy_exchange",type = ExchangeTypes.FANOUT)
))
6.消费者可靠性
(1)消费者确认机制
SpringAMOP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:
- none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
- auto:自动模式。SpringAMOP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack(建议使用的)
当业务出现异常时,根据异常判断返回不同结果:- 如果是业务异常,会自动返回nack
- 如果是消息处理或校验异常,自动返回reiect
application.yml
spring:
rabbitmq:
listener:
simple:
#消息确认模式,none:关闭ack,manual:手动ack,auto:自动ack(建议使用)
acknowledge-mode: auto
(2)失败重试机制
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
application.yml
spring:
rabbitmq:
listener:
simple:
#每次只能获取一条消息,处理完成才能获取下一条消息
prefetch: 1
#消息确认模式,none:关闭ack,manual:手动ack,auto:自动ack(建议使用)
acknowledge-mode: auto
retry:
#开启失败重试机制
enabled: true
#失败后的初始等待时间
initial-interval: 1000ms
#失败后下次的等待时长倍数
multiplier: 1
#最大重试次数
max-attempts: 3
#true无状态;false有状态。如果业务中包含事务,这里改为false
stateless:true
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机,可以交由人工手动处理(当前采用此种方式)
Configuration
@Configuration
public class ErrorConfig {
@Bean
public DirectExchange errorExchange() {
return new DirectExchange("error_direct");
}
@Bean
public Queue errorQueue() {
return new Queue("error_queue");
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorExchange) {
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
System.err.println("执行messageRecoverer。。。");
return new RepublishMessageRecoverer(rabbitTemplate,"error_direct","error");
}
}
7.延迟消息队列
(1)死信交换机
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)。
(2)延迟消息插件
该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。
安装插件
- 下载插件rabbitmq_delayed_message_exchange-3.13.0.ez 到plugins目录下
- 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 重启rabbitMQ
生产者
@Test
void testDelayQueue() {
//队列名称
String exchangeName = "delay_exchange";
//消息内容
String message = "Hello World!";
//发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend(exchangeName,"delay", message,new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
消费者
delayed = "true"
设置交换机类型为x-delayed-message
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="delay_queue",durable = "true"),
exchange = @Exchange(name="delay_exchange",delayed = "true"),
key = "delay"
))
public void getDelayMessage(String message) {
log.info("接收到的延迟消息=>{}",message);
}