Spring Boot整合RabbitMQ

大佬文章,请优先查看!!!

黑马程序员RabbitMQ入门到实战教程,MQ消息中间件



简述

  • Spring支持

    spring-jms提供了对JMS的支持
    spring-rabbit提供了对AMQP的支持
    需要ConnectionFactory的实现来连接消息代理
    提供JmsTemplate、RabbitTemplate来发送消息
    @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
    @EnableJms、@EnableRabbit开启支持

  • Spring Boot自动配置

    JmsAutoConfiguration
    RabbitAutoConfiguration

自定义消息转换器

spring的对象系处理器是由org.springframework.amqp.support.converter.MessageConverter来处理的,而默认实现就是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化,如果要修改只需要定义一个MessageConverter类型的Bean即可。

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
@Configuration
public class MyAMQPConfig {
/**
* json格式消息转换
*
* @return
*/
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}

rabbitmq配置说明

生产者重连

当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。

如果对于业务性能有要求,建议禁用重试机制。如果需要使用,需要合理配置等待时长和重试次数,也可以考虑使用异步线程来执行发送消息。

#开启超时重试机制
spring.rabbitmq.template.retry.enabled=true
#失败后的初始等待时间
spring.rabbitmq.template.retry.initial-interval=1000
#失败后下次的等待时长倍数,下次等待时长=initial-interval * multiplier
spring.rabbitmq.template.retry.multiplier=1
#最大重试次数
spring.rabbitmq.template.retry.max-attempts=3

生产者确认机制

RabbitMQ中有 Publisher ConfirmPublisher Return两种确认机制。开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:

  • 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功
  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功
  • 其他情况都会返回NACK,告知投递失败
spring:
rabbitmq:
publisher-confirm-type: correloted #开启publisher confirm消息确认模式,并设置confirm类型
publisher-returns: true #开启publisher return机制,一般不会开启

publisher-confirm-type有三种模式:

none:关闭confirm机制

simple:同步阻塞等待MQ的回执消息

correlated:MQ异步回调方式返回回执消息

Return机制

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey)->{
log.info("消息发送失败,应答码:{},原因:{},交换机:{},路由键:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());
});
}
}
Confirm机制

发型消息,指定消息ID、消息ConfirmCallback

@Test
public void testPublisherConfirm() throws InterruptedException {
// 创建CorrelationData
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
// Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable e) {
// Future发送异常时的处理逻辑,属于spring处理异常,不是mq返回的失败,基本不会触发
log.error("handle message ack fail", e);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// Future接收到回执的处理逻辑,参数中的result就是回执内容
if (result.isAck()) {
log.debug("发送消息成功,收到ack...");
} else {
log.error("发送消息失败,收到nack, reason:{}", result.getReason());
}
}
});
// 发送消息
rabbitTemplate.convertAndSend("exchange", "routingkey", "hello", cd);
}
小结

生产者消息确认需要额外的网路和系统资源开销,尽量不要使用。如果需要使用,无需开启Publisher-Return机制,因为一般路由失败是自身业务问题。对于nack消息可以有限次数重试,依然失败则记录异常消息。

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMQP已经实现了消息确认功能,并允许我们通过配置文件选择ACK处理方式,有三种方式:

  • none:不处理。即消息投递给消费者后立即ack,消息会立刻从MQ删除。非常不安全,不建议使用。

  • manual:手动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果:

    如果是业务异常,会自动返回nack。

    如果是消息处理或校验异常,自动返回reject。

spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: none # none:关闭ack;manual:手动ack;auto:自动ack

消费者失败重试机制

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理压力飙升。

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1秒
multiplier: 1 # 下次失败的等待时长倍数,下次等待时长=multiplier*lost-interval
max-attempts: 3 # 最大重试次数
stateless: true # true:无状态;false:有状态。如果业务中包含事务,这里改为false
失败消息处理策略

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式。
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。

RepublishMessageRecoverer方式示例:

将失败处理策略改为RepublishMessageRecoverer,首先定义接收失败消息的交换机、队列及其绑定关系;然后定义RepublishMessageRecoverer

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate reabbitTempate) {
return new RepublishMessageRecoverer(rabbitTemplate, "hguo.error.exchanges", "error.routing.key");
}

RabbitMQ整合

引入spring-boot-starter-amqp依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.yml配置

spring:
rabbitmq:
host: # 主机地址
username: # 用户名guest
password: # 密码guest
port: 5672 # 默认端口5672

启动类添加启动注解

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 自动配置
* 1、RabbitAutoConfiguration
* 2、有自动配置了连接工厂ConnectionFactory;
* 3、RabbitProperties 封装了 RabbitMQ的配置
* 4、 RabbitTemplate :给RabbitMQ发送和接受消息;
* 5、 AmqpAdmin : RabbitMQ系统管理功能组件;
* AmqpAdmin:创建和删除 Queue,Exchange,Binding
* 6、@EnableRabbit + @RabbitListener 监听消息队列的内容
*
*/
@EnableRabbit //开启基于注解的RabbitMQ模式
@SpringBootApplication
@MapperScan({"com.example.demo.mapper"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

MQ配置(队列、交换机声明)

import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* rabbitMq配置
*
*/
@Configuration
public class RabbitMqConfig {
private final CachingConnectionFactory connectionFactory;
public RabbitMqConfig(CachingConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
/**
* 消息监听器工厂
*
* @return
*/
@Bean(name = "mqListenerContainer")
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 限流 一次性从队列中最大能拉取消息数
factory.setPrefetchCount(50);
return factory;
}
// 其他方式声明监听器
/* public SimpleMessageListenerContainer getObject() throws Exception {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setAmqpAdmin(amqpAdmin);
container.setConnectionFactory(connectionFactory);
container.setQueues(queue);
container.setPrefetchCount(20);
container.setConcurrentConsumers(20);
container.setMaxConcurrentConsumers(100);
container.setDefaultRequeueRejected(Boolean.FALSE);
container.setAdviceChain(createRetry());
container.setAcknowledgeMode(autoAck ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL);
// container.stop();
if (Objects.nonNull(consumer)) {
container.setMessageListener(consumer);
}
return container;
}*/
/**
* 动态生成队列
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
/**
* 声明队列
*
* @return
*/
@Bean
public Queue createQueue() {
/*
* 第一种方式:
*
* durable():代表需要持久化
* exclusive(): 代表该队列独占(只允许有一个consumer监听)
* autoDelete(): 代表需要自动删除(没有consumer自动删除)
* withArgument(): 队列的其他参数
*/
// return QueueBuilder.durable("boot_work_queue").exclusive().autoDelete().withArgument("key", "val").build();
/*
* 第二种方式:通过new Queue对象来创建队列
*
* name:队列名称
* durable:队列是否持久化,默认是true
* exclusive:是否独占,队列是否设置为排他队列,默认是false。为true时设置为排他队列,只对首次声明它的连接可见,
* 其他连接无法声明相同名称的其他队列,并且在连接断开时自动删除,即使持久化也会被删除
* autoDelete:队列是否自动删除,默认false。为true时,当没有消费者使用此队列,该队列会自动删除
*
* 一般设置一下队列的持久化就好,其余两个就是默认false
* */
return new Queue("chat.room.queue", true, false, false);
}
/**
* 声明死信队列
*
* @return
*/
@Bean
public Queue deadQueue() {
Map<String, Object> map = new HashMap<>();
// 队列中的每一个消息未被消费则5秒后过期,被自动删除并移到死信队列
map.put("x-message-ttl", 5000);
return new Queue("chat.dead.queue", true, false, false, map);
}
/**
* 声明发布订阅模式交换机
*
* @return
*/
@Bean
FanoutExchange fanoutExchange() {
/*
* 第一种方式: 通过ExchangeBuilder构建交换机
*
* 通过ExchangeBuilder声明交换机
* 每种类型交换机有对应方法,如:fanoutExchange()、topicExchange()
* - durable: 是否持久化
* - autoDelete: 是否自动删除
* - withArgument: 交换机其他参数
* */
// return ExchangeBuilder.fanoutExchange("boot_fanout_exchange").durable(true).build();
// return ExchangeBuilder.directExchange("boot_direct_exchange").durable(true).autoDelete().withArgument("key","val").build();
/*
* 第二种方式:通过new FanoutExchange对象声明交换机
*
* name:交换机名称
* durable:是否持久化(默认false)
* autoDelete:是否自动删除(默认false)
* */
return new FanoutExchange("fanout.exchange", true, false);
}
/**
* 声明路由模式交换机
*
* @return
*/
@Bean
DirectExchange directExchange() {
return new DirectExchange("direct.exchange", true, false);
}
/**
* 声明主题模式交换机
*/
@Bean
TopicExchange topicExchange() {
return new TopicExchange("topic.exchange", true, false);
}
/**
* 交换机与队列进行绑定
*/
@Bean
public Binding bindQueueExchange() {
/*
* 第一种方式: 通过BindingBuilder绑定
*
* bind(Queue): 需要绑定的queue
* to(Exchange): 需要绑定到哪个交换机
* with(String): routing key
* noargs(): 进行构建
*/
// return BindingBuilder.bind(testQueue()).to(directExchange()).with("article").noargs();
// return BindingBuilder.bind(testQueue()).to(directExchange()).with(testQueue().getName());
/*
* 第二种方式:通过new Binding对象绑定
*
* destination: 绑定的队列
* destinationType: 绑定的类型 Binding.DestinationType.QUEUE: 绑定的类型为queue(交换机不仅可以绑定queue还可以绑定exchange)
* exchange: 哪个交换机需要绑定
* routingKey: routing key
* arguments: 其他参数
*/
return new Binding("chat.room.queue", Binding.DestinationType.QUEUE, "fanout.exchange", "chat.room.key", null);
}
}

动态创建队列与交换机

一般都是通过注入Bean的方式去声明交换机、队列,应用启动时去自动创建。

如果需要动态创建,比如通过接口、或者业务代码自己去操作,这个使用就需要使用RabbitMQ提供的操作接口。如果是基于Spring Boot,则可以直接使用其提供的AmqpAdmin,其对RabbitMQ的原生接口进行了二次封装,使用起来十分方便。AmqpAdmin接口,只有一个实现类RabbitAdmin

import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
/**
* RabbitMq动态配置
* 动态生成队列和交换机
*
*/
@Controller
public class RabbitMqDynamicController {
/**
* 实现了AmqpAdmin接口
*/
@Autowired
private RabbitAdmin rabbitAdmin;
/**
* 通过AmqpAdmin:创建和删除 Queue,Exchange,Binding
*/
@Autowired
private AmqpAdmin amqpAdmin;
@PostMapping("/dynamic")
public void dynamicConfig() {
//创建mq队列
Queue test3Queue = new Queue("test3_Queue", true, false, false);
rabbitAdmin.declareQueue(test3Queue);
//创建交换机
DirectExchange direct1Exchange = new DirectExchange("direct1_Exchange", true, false);
rabbitAdmin.declareExchange(direct1Exchange);
//绑定交换机和队列,并设置Routing key
Binding binding = BindingBuilder.bind(test3Queue).to(direct1Exchange).with(test3Queue.getName());
rabbitAdmin.declareBinding(binding);
}
/**
* 删除mq队列
*
* @return
*/
@PostMapping("/deleteMq")
public String deleteMq(String mq) {
rabbitAdmin.deleteQueue(mq);
return "ok";
}
@PostMapping("/")
public void amqpAdminCreate(){
// 创建Exchange
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
/*
* 创建消息队列
* - Queue是类可以直接new,构造器第一个参数:队列名,第二参数:是否持久化存在,若没有指定参数则随机给队列名
* */
amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));
/*
* 创建绑定规则
* - 参数1:目的地(队列)
* - 参数2:绑定的类型->队列
* - 参数3:Exchange
* - 参数4:路由件
* - 参数5:参数没有为null
* */
amqpAdmin.declareBinding(new Binding("amqpadmin.queue", Binding.DestinationType.QUEUE,"amqpadmin.exchange","amqp.haha",null));
//amqpAdmin.deleteExchange(); // 删除交换器
//amqpAdmin.deleteQueue(); // 删除队列
}
}

自定义消息转换器

默认使用jdk序列化方式

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 消息转换器
*
*/
@Configuration
public class MessageConverterConfig {
/**
* 使用json转换方式
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}

消息实体

注意:如果消费者接受的消息是实体类对象,需要将类序列化

import lombok.Data;
import java.io.Serializable;
/**
* 测试:消息实体
*
*/
@Data
public class Book implements Serializable {
private static final long serialVersionUID = 7512574760433830393L;
/**
* 书名
*/
private String name;
/**
* 价格
*/
private String price;
}

生产者推送消息

import com.alibaba.fastjson.JSON;
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 推送消息控制器
*
*/
@RestController
@Slf4j
@RequestMapping("/producer")
public class ProducerController {
private final RabbitTemplate rabbitTemplate;
public ProducerController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@PostMapping("/testSend")
public void testSendMessage(@RequestBody Book book) {
log.info("生产者发送消息:{}", JSON.toJSONString(book));
/*
* 对象被默认序列化以后发送出去
* object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq
* */
rabbitTemplate.convertAndSend("chat.fanout.exchange", "chat.room.key", book);
}
}

消费者监听消息

  • 方式一
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 使用@RabbitListener注解在类上监听
*
*/
@Component
@RabbitListener(queues = "chat.room.queue")
@Slf4j
public class TestMqReceiver {
/**
* 监听消息
*
* @param message 消息体
*/
@RabbitHandler
public void process(Book message) {
log.info("@RabbitListener注解在类上监听消息:{}", message);
}
}
2023-12-07 22:37:14.414 INFO 20184 --- [nio-8001-exec-2] c.m.r.controller.ProducerController : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:37:18.695 INFO 20184 --- [ntContainer#0-1] c.m.rabbitmq.listener.TestMqReceiver : @RabbitListener注解在类上监听消息:Book(name=神雕侠侣, price=100)
  • 方式二
import com.alibaba.fastjson.JSONObject;
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 使用@RabbitListener注解方法上监听消息
*
*/
@Component
@Slf4j
public class TestMqReceiver2 {
/**
* 监听指定的消息队列(数组类型,可以指定监听多个)
*
* @param book
*/
@RabbitListener(queues = {"chat.room.queue"})
public void receive(Book book) {
log.info("使用@RabbitListener注解方法上监听消息:{}", JSONObject.toJSONString(book));
}
}
2023-12-07 22:37:29.836 INFO 20184 --- [nio-8001-exec-1] c.m.r.controller.ProducerController : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:37:29.838 INFO 20184 --- [ntContainer#1-1] c.m.rabbitmq.listener.TestMqReceiver2 : 使用@RabbitListener注解方法上监听消息:{"name":"神雕侠侣","price":"100"}
  • 方式三
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* rabbitTemplate.receiveAndConvert()方法接收消息
*
*/
@Controller
@RequestMapping("/testMqReceiver3")
@Slf4j
@RequiredArgsConstructor
public class TestMqReceiver3 {
private final RabbitTemplate rabbitTemplate;
/**
* 接收数据
*/
@GetMapping("/receive")
public Object receiveMessage() {
Object message = rabbitTemplate.receiveAndConvert("chat.room.queue");
log.info("receiveAndConvert方法方式监听消息:{}", JSON.toJSONString(message));
return message;
}
}
2023-12-07 22:43:28.158 INFO 15420 --- [nio-8001-exec-1] c.m.r.controller.ProducerController : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:43:28.171 INFO 15420 --- [nio-8001-exec-1] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.56.103:5672]
2023-12-07 22:43:28.199 INFO 15420 --- [nio-8001-exec-1] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#59ed3e6c:0/SimpleConnection@133f6b8e [delegate=amqp://guest@192.168.56.103:5672/, localPort= 61478]
2023-12-07 22:44:04.910 INFO 15420 --- [nio-8001-exec-2] c.m.r.controller.ProducerController : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:44:18.177 INFO 15420 --- [nio-8001-exec-3] c.m.rabbitmq.listener.TestMqReceiver3 : receiveAndConvert方法方式监听消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:44:25.370 INFO 15420 --- [nio-8001-exec-4] c.m.rabbitmq.listener.TestMqReceiver3 : receiveAndConvert方法方式监听消息:{"name":"神雕侠侣","price":"100"}
posted @   Lz_蚂蚱  阅读(203)  评论(0编辑  收藏  举报
(评论功能已被禁用)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起