rabbitmq官方文档
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成, 这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可 能需要持久性存储)等。
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直 在队列里面,等待消费者连接到这个队列将其取走。
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交 换器理解成一个由绑定构成的路由表。
Exchange 和Queue的绑定可以是多对多的关系。
网络连接,比如一个TCP连接。
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道 发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都 是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加 密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥 有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时 指定,RabbitMQ 默认的 vhost 是 / 。
表示消息队列服务器实体
应用场景一:缩短调用时间
同步处理:1、注册信息写入数据库;2、发送注册邮件;3、发送注册短信 150S
异步处理【需要等待返回】:1、注册信息写入数据库; 异步发送: 2、发送注册邮件 + 发送注册短信 100S
消息队列【不需要等待返回】:1、注册信息写入数据库; 写入队列:异步读取 2、发送注册邮件;3、发送注册短信 50S
【因为 发送邮件+发送短信 不需要等待返回】
应用场景二:应用解耦
例如订单系统 调用库存系统,如果库存系统升级,会导致订单系统要修改源代码
订单系统往消息队列写入一条消息【不关心库存系统的迭代】,由库存主动取出消息【实时订阅消息】
应用场景三:流量控制【削峰】
秒杀:百万请求 存储到消息队列,由秒杀业务订阅 队列一条条处理,不会导致流量过大使服务器宕机
AMQP 中的消息路由
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routingkey 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词。
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
#设置开机随docker启动
docker update rabbitmq --restart=always
4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
rabbitmq官方文档
切换到“Queues”标签,可以查看队列信息,点击队列名称,可查看队列所有状态的消息数量和大小等统计信息
切换到“Exchanges”标签,可查看和管理交换器,单击交换器名称,可查看到更多详细信息,比如交换器绑定,还可以添加新的绑定
按照下图接口新建交换机,队列测试
只有atguigu队列拿到了消息
所有队列都收到了消息
如果填写的路由键不存在,将无法发送消息
路由键存在,以atguigu开头的队列都收到了消息
如果路由键与队列名完全相同,那么与direct的效果是一致的
gulimall-order/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
spring:
rabbitmq:
host: 192.168.157.128
port: 5672
virtual-host: /
@EnableRabbit
可创建交换机类型
package site.zhourui.gulimall.order;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Exchange;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
/**
* 创建交换机
*/
@Test
void createExchange() {
// public DirectExchange(String name, 交换机名称
// boolean durable, 是否持久化
// boolean autoDelete, 是否自动删除
// Map arguments) 参数
DirectExchange directExchange = new DirectExchange("test_exchange",false,false,null);
amqpAdmin.declareExchange(directExchange);
}
}
@Test
void createQueue() {
// public Queue(String name, 队列名称
// boolean durable, 是否持久化
// boolean exclusive, 是否是排他队列(只能被一个consumer的连接占用)
// boolean autoDelete, 是否自动删除
// @Nullable Map arguments) 参数
Queue queue = new Queue("test_queue",true,false,false);
amqpAdmin.declareQueue(queue);
}
@Test
void createBinding() {
// public Binding(String destination 【目的地,队列name或 交换机name(如果作为路由的话)】
// Binding.DestinationType destinationType, 【目的地类型 queue还是exchange(路由)】
// String exchange, 【交换机】
// String routingKey, 【路由键】
// @Nullable Map arguments) 【自定义参数】
Binding binding = new Binding("test_queue", Binding.DestinationType.QUEUE,"test_exchange","test.binding",null);
amqpAdmin.declareBinding(binding);
}
如果消息内容是一个对象,那么该对象必须实现Serializable接口,因为convertAndSend()默认使用的jdk序列化
@Test
void sendMessage() {
// public void convertAndSend(String exchange, 交换机
// String routingKey, 路由键
// Object message, 发送的消息
// MessagePostProcessor messagePostProcessor) 消息序列化器
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(1L);
orderReturnReasonEntity.setName("sendMessageTest");
orderReturnReasonEntity.setCreateTime(new Date());
rabbitTemplate.convertAndSend("test_exchange","test.binding",orderReturnReasonEntity);
log.info("消息发送完成");
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyRabbitConfig.java
package site.zhourui.gulimall.order.config;
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;
/**
* @author zr
* @date 2021/12/15 9:56
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
再次发送消息测试
@Test
void sendMessage() {
// public void convertAndSend(String exchange, 交换机
// String routingKey, 路由键
// Object message, 发送的消息
// MessagePostProcessor messagePostProcessor) 消息序列化器
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(1L);
orderReturnReasonEntity.setName("sendMessageTest2");
orderReturnReasonEntity.setCreateTime(new Date());
rabbitTemplate.convertAndSend("test_exchange","test.binding",orderReturnReasonEntity);
log.info("消息发送完成");
}
测试结果
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/TestListener.java
主启动类上需要加上
@EnableRabbit
在需要监听消息的方法上加上
@RabbitListener
,并指明监听队列名称
@RabbitListener(queues = "test_queue")
void receiveMessage(Object msg) {
log.info("收到消息内容:"+msg+"==>类型:"+msg.getClass());
}
监听结果
收到消息内容:(Body:'{"id":1,"name":"sendMessageTest2","sort":null,"status":null,"createTime":1639536120687}' MessageProperties [headers={__TypeId__=site.zhourui.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=test_exchange, receivedRoutingKey=test.binding, deliveryTag=2, consumerTag=amq.ctag-OKodkqdQj7sbMy6xgVex5A, consumerQueue=test_queue])==>类型:class org.springframework.amqp.core.Message
上一步接收到消息还需要手动封装为对应对象,只需要将序列化的类放在message后就可以自动封装为对应对象(消息接收对象必须与消息发送对象一致)
Message对象可以拿到消息的所有信息
@RabbitListener(queues = "test_queue")
void receiveMessage2(Message msg,OrderReturnReasonEntity orderReturnReasonEntity) {
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
log.info("收到消息内容:"+msg+"==>内容"+orderReturnReasonEntity);
}
监听结果
收到消息内容:(Body:'{"id":1,"name":"sendMessageTest2","sort":null,"status":null,"createTime":1639539092604}' MessageProperties [headers={__TypeId__=site.zhourui.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=test_exchange, receivedRoutingKey=test.binding, deliveryTag=2, consumerTag=amq.ctag-58WYA7sSHI64OWjRLJpc9w, consumerQueue=test_queue])==>内容OrderReturnReasonEntity(id=1, name=sendMessageTest2, sort=null, status=null, createTime=Wed Dec 15 11:31:32 CST 2021)
参数类型
1、Mcssage message:原生消息详细信息。头+体
2、T<发送的消息的类型>orderReturnReasonEntity content;3、Channel channel:当前传输数据的通道
3、Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息场景:
1)、订单服务启动多个;同一个消息,只能有一个客户端收到
2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/TestListener.java
@RabbitListener(queues = "test_queue")
void receiveMessage3(Message msg,
OrderReturnReasonEntity orderReturnReasonEntity,
Channel channel) {
log.info("信道:"+channel);
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
log.info("收到消息内容:"+msg+"==>内容"+orderReturnReasonEntity);
}
新建一个测试消息发送controller
gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/TestSendMessageController.java
调用接口:http://localhost:9000/sendMessage?num=5发送多条消息
package site.zhourui.gulimall.order.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.common.utils.PageUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.order.entity.OrderReturnReasonEntity;
import java.util.Date;
import java.util.Map;
/**
* @author zr
* @date 2021/12/15 14:12
*/
@RestController
public class TestSendMessageController {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 列表
*/
@RequestMapping("/sendMessage")
public R sendMany(@RequestParam("num") Integer num){
for (int i = 0; i <num; i++) {
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(1L);
orderReturnReasonEntity.setName("消息---"+i);
orderReturnReasonEntity.setCreateTime(new Date());
rabbitTemplate.convertAndSend("test_exchange","test.binding",orderReturnReasonEntity);
}
return R.ok();
}
}
复制一个订单模块,并启动
调用消息发送接口发送10条消息
http://localhost:9000/sendMessage?num=10
结论:每个客户端都是接收的同一个队列,但是没有重复的消息
并且在此期间只创建了两个连接,确认了一个客户端只有一个连接的说法
@RabbitListener(queues={“hello-java-queue”})放在类上【作用:用来指定接收哪个队列的消息】
@RabbitHandler:标在方法上【作用:重载处理不同类型的数据】
修改发送消息的接口,使其能够发送不同类型数据的消息
gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/TestSendMessageController.java
package site.zhourui.gulimall.order.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.common.utils.PageUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.entity.OrderReturnReasonEntity;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* @author zr
* @date 2021/12/15 14:12
*/
@RestController
public class TestSendMessageController {
@Autowired
RabbitTemplate rabbitTemplate;
@RequestMapping("/sendMessage")
public R sendMany(@RequestParam("num") Integer num){
for (int i = 0; i <num; i++) {
//偶数,消息就发送orderReturnReasonEntity对象
if (i%2==0){
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(1L);
orderReturnReasonEntity.setName("orderReturnReasonEntity消息---" + i);
orderReturnReasonEntity.setCreateTime(new Date());
rabbitTemplate.convertAndSend("test_exchange", "test.binding", orderReturnReasonEntity);
}else {
//基数,消息就发送orderEntity对象
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString()+"orderEntity消息---"+i);
rabbitTemplate.convertAndSend("test_exchange", "test.binding", orderEntity);
}
}
return R.ok();
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/TestListener.java
package site.zhourui.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.entity.OrderReturnReasonEntity;
/**
* @author zr
* @date 2021/12/15 11:50
*/
@Service
@Slf4j
@RabbitListener(queues = "test_queue")
public class TestListener {
// @RabbitListener(queues = "test_queue")
// void receiveMessage(Object msg) {
// log.info("收到消息内容:"+msg+"==>类型:"+msg.getClass());
// }
// @RabbitListener(queues = "test_queue")
// void receiveMessage2(Message msg,OrderReturnReasonEntity orderReturnReasonEntity) {
// byte[] body = msg.getBody();
// MessageProperties messageProperties = msg.getMessageProperties();
// log.info("收到消息内容:"+msg+"==>内容"+orderReturnReasonEntity);
// }
// @RabbitListener(queues = "test_queue")
@RabbitHandler
void receiveMessage3(Message msg,
OrderReturnReasonEntity orderReturnReasonEntity,
Channel channel) {
// log.info("信道:"+channel);
// byte[] body = msg.getBody();
// MessageProperties messageProperties = msg.getMessageProperties();
log.info("==>内容"+orderReturnReasonEntity);
}
@RabbitHandler
void receiveMessage4(Message msg,
OrderEntity orderEntity,
Channel channel) {
// log.info("信道:"+channel);
// byte[] body = msg.getBody();
// MessageProperties messageProperties = msg.getMessageProperties();
log.info("==>内容"+orderEntity);
}
}
发送消息测试http://localhost:9000/sendMessage?num=10
同一个队列拿出不同类型的数据,并且可以封装为对应的对象,做不同的处理,只使用RabbitListener是达不到这个效果的,
但是如果接收消息与发送消息拿到的对象是自己将json封装的对象那么就这个功能用处就不大了
开启配置
rabbitmq:
host: 192.168.157.128
port: 5672
virtual-host: /
#开启发送端确认
publisher-confirms: true
自定义RabbitTemplate,开启确认模式
package site.zhourui.gulimall.order.config;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author zr
* @date 2021/12/15 9:56
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、服务收到消息就会回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 1、spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
}
开启配置
server:
port: 9000
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.157.128:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
rabbitmq:
host: 192.168.157.128
port: 5672
virtual-host: /
#开启发送端确认
publisher-confirms: true
# 开启发送端消息抵达Queue确认
publisher-returns: true
# 只要消息抵达Queue,就会异步发送优先回调returnfirm
template:
mandatory: true
自定义RabbitTemplate,开启回退模式
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyRabbitConfig.java
package site.zhourui.gulimall.order.config;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author zr
* @date 2021/12/15 9:56
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、服务收到消息就会回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 1、spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +
"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
});
}
}
因为该模式需要消息代理投递给指定队列失败才会触发,所以此处我们故意将路由键写错,此次测试后将错误改回来
错误消息
Fail Message[(Body:’{“id”:1,“name”:“消息—0”,“sort”:null,“status”:null,“createTime”:1639576478075}’ MessageProperties [headers={TypeId=site.zhourui.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]>replyCode[312]>replyText[NO_ROUTE]>exchange[test_exchange]>routingKey[test.binding222]
confirm…correlationData[null]>ack:[true]>cause:[null]
在消息发送的时候CorrelationData参数就是消息的唯一id
这个id在发送端确认时是可以拿到的,排查那些消息未成功抵达是可以用来排查(与接收到的存放在数据库的消息唯一id进行对比)
目前存在的问题:
默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息,此时会出现一个问题:
在接收消息这里打上断点,将第一个消息走完,这时模拟突发状况,比如服务器宕机,关掉服务器
关掉服务器后剩余的4个消息也都消失了,也没有经过消费端确认,相当于丢失了
为了解决这个问题,我们将自动ack确认改为手动确认,只有手动ack确认的消息才能被队列移除或者再次放入队列
开启手动ack机制
server: port: 9000 spring: datasource: username: root password: root url: jdbc:mysql://192.168.157.128:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver cloud: nacos: discovery: server-addr: 127.0.0.1:8848 rabbitmq: host: 192.168.157.128 port: 5672 virtual-host: / #开启发送端确认 publisher-confirms: true # 开启发送端消息抵达Queue确认 publisher-returns: true # 只要消息抵达Queue,就会异步发送优先回调returnfirm template: mandatory: true # 使用手动ack确认模式,关闭自动确认【消息丢失】 listener: simple: acknowledge-mode: manual
发送五个消息测试,就算客户端已经拿到了消息,但是没有确认,队列中的消息仍然不能移除,只不过状态由ready变为unacked
此时关闭服务服务,消息的状态由unacked变为ready,下次客户端服务启动又会接收到消息ready变为unacked
除非手动确认
package site.zhourui.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.entity.OrderReturnReasonEntity;
import java.io.IOException;
/**
* @author zr
* @date 2021/12/15 11:50
*/
@Service
@Slf4j
@RabbitListener(queues = "test_queue")
public class TestListener {
// @RabbitListener(queues = "test_queue")
// void receiveMessage(Object msg) {
// log.info("收到消息内容:"+msg+"==>类型:"+msg.getClass());
// }
// @RabbitListener(queues = "test_queue")
// void receiveMessage2(Message msg,OrderReturnReasonEntity orderReturnReasonEntity) {
// byte[] body = msg.getBody();
// MessageProperties messageProperties = msg.getMessageProperties();
// log.info("收到消息内容:"+msg+"==>内容"+orderReturnReasonEntity);
// }
// @RabbitListener(queues = "test_queue")
@RabbitHandler
void receiveMessage3(Message msg,
OrderReturnReasonEntity orderReturnReasonEntity,
Channel channel) {
System.out.println("接收到消息:---"+orderReturnReasonEntity);
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
log.info("==>处理完成消息"+orderReturnReasonEntity.getName());
long deliveryTag = messageProperties.getDeliveryTag();
// public void basicNack(long deliveryTag, //channel内按顺序自增
// boolean multiple) //是否批量确认
try {
//debug模式无法模拟真实情况下的宕机,关闭了也会继续执行下去,这里模拟突然宕机部分消息未接到
if(deliveryTag%2==0){
channel.basicAck(deliveryTag,false); //手动ack确认接收消息
System.out.println("签收了货物---"+deliveryTag);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// @RabbitHandler
// void receiveMessage4(Message msg,
// OrderEntity orderEntity,
// Channel channel) {
log.info("信道:"+channel);
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
// log.info("==>内容"+orderEntity);
// }
}
断点测试一个一个的放消息手动确认消息
debug模式无法模拟真实情况下的宕机,关闭了也会继续执行下去,这里模拟突然宕机部分消息未接到,这里测试可以放开断点,
此时只签收了货物—2,货物—4
还有三个未确认
关闭客户服务端,消息状态由unacked->ready
剩余的消息仍然可以继续签收
channel内按顺序自增
个人理解:相当于channel信道中消息的唯一id
上面为什么重启了就能再接收一个消息了?
之前的发送的五个消息,只接受偶数项消息(2,4),还剩3条消息,客户端重启后剩余消息在channel中又重新排序(1,2,3),随意再次接收到一个消息
package site.zhourui.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.entity.OrderReturnReasonEntity;
import java.io.IOException;
/**
* @author zr
* @date 2021/12/15 11:50
*/
@Service
@Slf4j
@RabbitListener(queues = "test_queue")
public class TestListener {
// @RabbitListener(queues = "test_queue")
// void receiveMessage(Object msg) {
// log.info("收到消息内容:"+msg+"==>类型:"+msg.getClass());
// }
// @RabbitListener(queues = "test_queue")
// void receiveMessage2(Message msg,OrderReturnReasonEntity orderReturnReasonEntity) {
// byte[] body = msg.getBody();
// MessageProperties messageProperties = msg.getMessageProperties();
// log.info("收到消息内容:"+msg+"==>内容"+orderReturnReasonEntity);
// }
// @RabbitListener(queues = "test_queue")
@RabbitHandler
void receiveMessage3(Message msg,
OrderReturnReasonEntity orderReturnReasonEntity,
Channel channel) {
System.out.println("接收到消息:---"+orderReturnReasonEntity);
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
log.info("==>处理完成消息"+orderReturnReasonEntity.getName());
long deliveryTag = messageProperties.getDeliveryTag();
// public void basicNack(long deliveryTag, //channel内按顺序自增
// boolean multiple) //是否批量确认
try {
//debug模式无法模拟真实情况下的宕机,关闭了也会继续执行下去,这里模拟突然宕机部分消息未接到
if(deliveryTag%2==0){
channel.basicAck(deliveryTag,false); //手动ack确认接收消息
System.out.println("签收了货物---"+deliveryTag);
}else {
// public void basicNack(long deliveryTag, //channel内按顺序自增
// boolean multiple, //是否批量退货
// boolean requeue) //确认后是否重新入队 false丢弃
channel.basicNack(deliveryTag,false,false); //手动ack确认拒绝消息
// channel.basicReject();
System.out.println("拒接签收了货物---"+deliveryTag);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// @RabbitHandler
// void receiveMessage4(Message msg,
// OrderEntity orderEntity,
// Channel channel) {
log.info("信道:"+channel);
byte[] body = msg.getBody();
MessageProperties messageProperties = msg.getMessageProperties();
// log.info("==>内容"+orderEntity);
// }
}
清除之前测试的消息,再次发送消息测试,
因为这里的requeue为false,所有的消息都被清除了
当requeue=true,deliveryTag奇数的消息被拒收的消息重新入队deliveryTag变为偶数,被接收
定时任务时效性问题
场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案:spring的 schedule 定时任务轮询数据库
缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决:rabbitmq的消息TTL和死信Exchange结合
基础版
交换机与队列一对一,一台路由器路由一个队列
升级版
只需要一台交换机绑定多个队列
容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyRabbitMQConfig.java
package site.zhourui.gulimall.order.config;
/**
* @author zr
* @date 2021/12/28 16:47
*/
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/**
* 创建队列,交换机,延迟队列,绑定关系 的configuration
* 不会重复创建覆盖
* 1、第一次使用队列【监听】的时候才会创建
* 2、Broker没有队列、交换机才会创建
*/
@Configuration
public class MyRabbitMQConfig {
// @RabbitHandler
// public void listen(Message message){
// System.out.println("收到消息:------>"+message);
// }
/**
* 延时队列
* @return
*/
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> arguments = new HashMap<>();
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map arguments) 属性【TTL、死信路由、死信路由键】
*/
Queue queue = new Queue("order.delay.queue",true,false,false,arguments);
return queue;
}
/**
* 死信队列
* @return
*/
@Bean
public Queue orderReleaseQueue(){
Queue queue = new Queue("order.release.order.queue",true,false,false);
return queue;
}
/**
* 死信路由[普通路由]
* @return
*/
@Bean
public Exchange orderEventExchange(){
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map arguments
* */
TopicExchange topicExchange = new TopicExchange("order-event-exchange",true,false);
return topicExchange;
}
/**
* 交换机与延时队列的绑定
* @return
*/
@Bean
public Binding orderCreateOrderBinding(){
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map arguments
* */
Binding binding = new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
return binding;
}
/**
* 死信路由与普通死信队列的绑定
* @return
*/
@Bean
public Binding orderReleaseOrderBinding(){
Binding binding = new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
return binding;
}
}
启动订单服务后
队列
交换机
绑定关系
向延时队列发送一条消息
延时队列收到一条消息
一分钟后该消息变为死信,再将该消息转发给死信队列
死信队列收到消息
使用测试代码测试
发送消息
gulimall-order/src/main/java/site/zhourui/gulimall/order/web/HelloController.java
@Autowired
private RabbitTemplate rabbitTemplate;
@ResponseBody
@GetMapping(value = "/test/createOrder")
public String createOrderTest() {
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
接收消息
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyRabbitMQConfig.java
@RabbitListener(queues = "order.release.order.queue")
public void listen(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期订单消息,准备关闭订单:------>"+orderEntity.getOrderSn());
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}