在我们平时做项目的时候,我们总会需要调用消息中间件来在各个模块之间实现消息的传递,那么我们就需要充分了解SpringBoot项目整合RabbitMQ中间件的整体流程。(以谷粒商城项目为例)
步骤① 导入RabbitMQ依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
这里我们需要知道,SpringBoot官方已经为我们准备好了关于RabbitMQ的启动器,都作为高级消息队列,存在于spring-boot-starter-amqp依赖中。
步骤② 定制化RabbitTemplate
我们都知道,SpringBoot为我们提供了RabbitTemplate类,来帮助我们在Java代码中直接操作我们的RabbitMQ,但是,对于官方提供的原生RabbitTemplate我们需要为我们项目具体要求进行定制化调整(如果项目中不需要,这步骤可以忽略)
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* @PostConstruct :MyRabbitConfig对象创建完成以后,执行这个方法
* 1. 服务收到消息就回调:
* 1. spring.rabbitmq.publisher-confirms=true
* 2. 设置确认回调
* 2. 消息正确抵达队列进行回调
* 1. spring.rabbitmq.publisher-return=true
* spring.rabbitmq.template.mandatory=true
* 2. 设置确认回调ReturnCallback
* 3. 消费端确认(保证每一个消息被正确确认,然后服务端才能删除消息)
* 必须加上配置:spring.rabbitmq.listener.direct.acknowledge-mode=manual
* 1. 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
* 问题:我们收到一个消息,自动回复给服务器ack,只有一个消息处理成功,宕机了,发生消息丢失;
* 消费者手动确认模式,只要我们没有明确告诉MQ,消息被签收,没有ack,消息就一直是unacked状态。即使Consumer宕机,消息不会丢失,会重新
* 变为ready,下一次有新的Consumer连接进来就发给他
* 2. 如何签收
* channel.basicAck(deliverTag,false):签收 业务成功完成就签收
* channel.basicNack(deliveryTag,false,true):拒签 业务失败就拒签
*
*/
@PostConstruct
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*1.只要消息抵达Broker就ack=true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param b 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
/**
* 1.做好消息确认机制(publisher,consumer【手动ack】)
* 2.每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
*/
//服务器收到了
System.out.println("confirm...correlationData:" + correlationData + ",ack:" + b + ",cause:" + s);
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详细信息
* @param replyCode 回复状态码
* @param replyText 回复的文本内容
* @param exchange 当时这个消息发给哪个交换机
* @param routingKey 当时这个消息用哪个路由键发送的
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//报错误了,修改数据库当前消息的状态
System.out.println("Fail Message" + message + ",replyCode" + replyCode);
}
});
}
}
其实定制化RabbitTemplate主要调用的方法就是`initRabbitTemplate()` 重写方法
步骤③ 编写配置文件
spring.rabbitmq.host=你的RabbitMQ所在的服务器地址
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端消息抵达队列确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步方式优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true
#手动ack消息
spring.rabbitmq.listener.direct.acknowledge-mode=manual
步骤④ 装配组件
@Configuration
public class MyMQConfig {
@Bean
public Queue orderDelayQueue() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000);
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateOrder() {
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrder() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
@Bean
public Binding orderReleaseOther() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
@Bean
public Queue orderSeckillOrderQueue() {
return new Queue("order.seckill.order.queue", true, false, false, null);
}
@Bean
public Binding orderSeckillOrderQueueBinding() {
return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null);
}
}
这步我们需要分析出我们项目模块中所需要用到的队列(死信队列,延迟队列等)和交换机,这样我们在配置类中进行创建,在@Configuration+@Bean的注解驱动下,项目启动时就会自动将组件放到IOC容器进行管理,并且实现了动态绑定(Binding)
步骤⑤ 核心业务中使用RabbitMQ
示例代码中仅是一部分,并不代表整个业务完整流程
生产者发送消息:
public void closeOrder(OrderEntity entity) {
//查询当前这个订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
//发给MQ一个
try{
//TODO 保证消息一定会发送出去,每一个发送的消息都可以做好日志记录(给数据库保存每一个消息的详细信息)
//TODO 定期扫描数据库将失败的消息重新发一遍
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}catch (Exception e){
//TODO 将没发送成功的消息进行重试发送
}
}
}
消费者接受消息:
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message msg) throws IOException {
try{
orderService.createSeckillOrder(seckillOrder);
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
}catch (Exception e){
channel.basicReject(msg.getMessageProperties().getDeliveryTag(),true);
}
}
}
注意:消费者接收消息的时候,重点是在类上标注@RabbitListener(queues="")注解,代表这个类用来监听哪个队列;在具体的消费方法上标注@RabbitHandler 标明这个方法用来消费指定队列的消息.
此篇文章仅代表个人在项目中的操作理解,并不代表完全正确,如有错误,希望大家指出!