上一篇:RabbitMQ安装与原理详解
官方文档:http://next.rabbitmq.com/api-guide.html
API文档:https://rabbitmq.github.io/rabbitmq-java-client/api/current/
一、Java操作RabbitMQ(未使用SpringBoot)
1. 添加依赖
<!--rabbitMQ java客户端依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.1.1</version>
</dependency>
2. 与RabbitMQ建立连接
- 先创建连接工厂
ConnectionFactory
,并指定连接RabbitMQ的四要素(ip,端口,账号,密码) - 通过连接工厂创建连接对象
Connection
,再通过连接对象获取到信道对象Channel
- 通过信道对象实现对 RabbitMQ 的操作
- 关闭接连和信道
注意:有些类名与其它包中的重名了(比如java.sql.Connection,java.nio.channels.Channel等),导包的时候,一定是com.rabbitmq.client
包下的
private static void send() {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置连接信息
factory.setHost("192.168.245.128");//设置RabbitMQ所在机器的IP地址
factory.setPort(5672);//指定端口
factory.setUsername("root");//指定连接账号
factory.setPassword("123");//指定连接密码
Connection connection = null;
final Channel channel; //后面可能会在匿名内部类中使用,故设为常量
try {
//创建连接对象,用于连接到RabbitMQ
connection=factory.newConnection();
//创建通道对象
channel=connection.createChannel();
/**
* 在这里实现对RabbitMQ的操作,之后的代码,如果没有特殊说明,默认都是写在这里的
*/
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
if(channel != null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3. Channel
操作RabbitMQ
在获取到 Channel 对象后(channel),通过该对象的方法来操作 RabbitMQ,常用的方法有:
(1)创建队列
队列是单例的,如果多次创建同一个名字的队列,仍是原来的那个队列
创建一个指定名字的队列:
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments)
- queue:队列名
- durable:是否持久化,true表示开启持久化
- exclusive:是否排外,true表示排外,一个队列只允许一个消费者连接
- autoDelete:如果没有消费者连接是否自动删除队列
- arguments:指定参数,通常为null
创建一个随机的队列:
//创建一个随机的默认的队列,队列名是随机的,当然也可以自己指定队列名
String queueName=channel.queueDeclare().getQueue();
- 返回 String:队列的名字
(2)创建交换机
交换机是单例的,如果多次创建同一个名字的交换机,并不会改变原来的那个交换机
channel.exchangeDeclare(String exchange, String type, boolean durable)
- exchange:交换机的名称
- type:交换机类型
- durable:是否是持久化的消息
(3)将队列和交换机绑定到到某个RoutingKey中
无论是接收消息还是发送消息,必须保证交换机已经创建和队列已经创建并实现绑定
因此这个3个步骤一般是在项目启动时直接创建好,例如交给Spring在启动容器时就可以创建
注意:
- 无论是交换机还是队列都不会因为重复的创建而给覆盖(单例)
- 如果不能在项目启动时就创建好交换机和队列,以及绑定,那么建议在消息消费者中完成这些操作,如果这么做了就必须要先启动消费者(一般也是先启动消费者)
channel.queueBind(String queue, String exchange, String routingKey)
- queue:队列名,必须已经存在
- exchange:需要绑定的交换机名称,必须已经存在
- routionKey:RoutingKey 这个值取值任意但必须要与发送时完全一致
(4)发送消息
channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
- exchange:将消息发送的指定的队列中,必须已经存在
- routingKey:routingKey的名称,如果不指定exchange,则routingKey表示队列名字,直接往队列里发送
- props:消息属性
- body:具体的消息数据
注意:
- 使用
direct
消息模式时,必须要指定routingKey(路由键),将指定的消息绑定到指定的路由键上 - fanout模式的消息需要将一个消息同时绑定到多个队列中,因此这里不能创建并指定某个队列,即不绑定队列和交换机,方法中的routingKey为
""
- 在topic模式中必须要指定routingkey,并且可以同时指定多层的routingKey,每个层次之间使用点("
.
")分隔即可 例如:aa.bb.cc
(5)接收消息
channel.basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback)
- queue:队列名称,必须已经存在
- autoAck:是否自动确认消息 true表示自动确认 false表示手动确认
- consumerTag:消费的标签,用于区分不同的消费者
- callback:消息接收后的回调方法,新建一个DefaultConsumer(channel)对象,构造方法的参数为信道对象,并重写handleDelivery方法,在该方法中对消息进行处理
- handleDelivery方法的参数:
- consumerTag:标识信道中投递的消息,每个信道中,每条消息的 consumerTag 从 1 开始递增
- body:表示取到的消息的字节数组
- handleDelivery方法的参数:
注意:
- 消息消费者消费完成消息以后可以不关闭通道和链接,如果不关闭通道和链接那么消费者会不间断的接收消息,因为我们的消息接收底层会启动一个子线程,异步实现接收
- 使用Exchange的direct模式时接收者的RoutingKey必须要与发送时的RoutingKey完全一致否则无法获取消息,接收消息时队列名也必须要发送消息时的完全一致
- 使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。这种模式中,可以同时启动多个接收者,只要都绑定到同一个Exchang上,即可让所有接收者同时接收同一个消息,是一种广播的消息机制
- Topic模式的消息接收时必须要指定RoutingKey并且可以使用
#
和*
来做统配符号,#
表示通配任意一个单词,*
表示通配任意多个单词,例如aa.*.*
或者aa.#
都可以接收到 routingKey 为 aa.bb.cc 的发送者发送的消息
(6)举例
接收消息(先启动接收消息进行监听,再启动发送消息):
-
不经过交换机,直接接收名字为 myQueue 的队列中的消息:
//不经过交换机,直接接收名字为 myQueue 的队列中的消息 channel.basicConsume("myQueue",true,"",new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //获取到队列中的消息 String message=new String(body,"UTF-8"); } });
-
接收交换机类型为 direct 的交换机绑定的队列中的数据
//接收与类型为 fanout 的交换机绑定的队列中的数据 channel.queueDeclare("myDirectQueue", true, false, false, null); channel.exchangeDeclare("directExchange", "fanout", true); channel.queueBind("myDirectQueue", "directExchange", ""); channel.basicConsume("myDirectQueue", true, "", new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //获取到队列中的消息 String message=new String(body,"UTF-8"); } });
-
接收交换机类型为 fanout 的交换机绑定的队列中的数据
//接收与类型为 fanout 的交换机绑定的队列中的数据 String queueName=channel.queueDeclare().getQueue(); channel.exchangeDeclare("fanoutExchange", "fanout", true); channel.queueBind(queueName, "fanoutExchange", ""); channel.basicConsume(queueName, true, "", new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //获取到队列中的消息 String message=new String(body,"UTF-8"); } });
-
接收交换机类型为 topic 的交换机绑定的队列中的数据
String queueName = channel.queueDeclare().getQueue(); //创建一个交换机 channel.exchangeDeclare("topicExchange", "topic", true); //将队列和交换机绑定到到某个RoutingKey中 channel.queueBind(queueName, "topicExchange", "aa.*"); //接收消息 channel.basicConsume(queueName, true, "", new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //获取消息 String message = new String(body, "UTF-8"); /* 这里对消息进行处理 */ } });
-
给上面的消息消费者发送消息:
//定义消息数据 String message="这是发送的消息数据"; //不经过交换机,直接发送到名字为 myQueue 的队列中 channel.basicPublish("", "myQueue", null, message.getBytes("UTF-8")); //将消息发送到类型为 direct 的交换机中 channel.basicPublish("directExchange", "directRoutingKey", null, message.getBytes("UTF-8")); //将消息发送到类型为 fanout 的交换机中 channel.basicPublish("fanoutExchange", "", null, message.getBytes("UTF-8")); //将消息发送到类型为 topic 的交换机中 channel.basicPublish("topicExchange", "aa.bb", null, message.getBytes("UTF-8"));
4. 事务与消息确认模式
事务消息与数据库的事务类似,只是MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略。
RabbitMQ有两种方式来解决这个问题:
- 通过AMQP提供的事务机制实现;
- 使用Confirm发送方和接收方确认模式实现;
由于事务机制的性能很差,故使用较多的是Confirm发送方确认模式
(1)事务机制
事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
channel.txSelect()
:声明启动事务模式;channel.txComment()
:提交事务;channel.txRollback()
:回滚事务;
注意:要在消息发送之前启动信道的事务模式,发送完毕后要提交事务,否则不会发送成功
(2)发送者确认模式
Confirm发送方确认模式使用和事务类似,也是通过设置Channel进行发送方确认的,最终达到确保所有的消息全部发送成功
Confirm的三种实现方式:
开启发送方确认模式:channel.confirmSelect();
方式一:channel.waitForConfirms()
:普通发送方确认模式;
方式二:channel.waitForConfirmsOrDie()
:批量确认模式;
方式三:channel.addConfirmListener()
:异步监听发送方确认模式
使用方式:在发送消息前,开启发送方确认模式,在发送完毕后,进行消息的确认
方式一:
在推送消息之前,channel.confirmSelect()声明开启发送方确认模式,再使用channel.waitForConfirms()等待消息被服务器确认即可。
//开启消息确认模式
channel.confirmSelect();
//发送消息到指定队列
channel.basicPublish("", "directRoutingKey", null, message.getBytes("UTF-8"));
if (channel.waitForConfirms()) {
System.out.println("消息发送成功");
}
方式二:
channel.waitForConfirmsOrDie()
使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未被确认就会抛出IOException异常。
//开启消息确认模式
channel.confirmSelect();
for (int i = 0; i < 10000; i++) {
channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}
channel.waitForConfirmsOrDie(); //直到所有信息都发布,只要有一个未确认就会IOException
System.out.println("全部执行完成");
方式三:
异步模式的优点,就是执行效率高,不需要等待消息执行完,只需要监听消息即可
//开启消息确认模式
channel.confirmSelect();
//发送消息到指定队列
for (int i = 0; i < 10000; i++) {
channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}
//异步监听确认和未确认的消息
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//这里是确认的消息
System.out.println("成功确认的消息" + deliveryTag + "==> " + multiple);
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//这里是未确认的消息
System.out.println("未确认的消息");
}
});
handleAck()方法与handleNack()方法的参数:
- deliveryTag:表示第几条消息
- multiple:boolean 类型,表示是否批量处理了消息,true表示批量执行了deliveryTag这个值的消息和它之前的所有消息,false的话表示单条确认
(3)消费者确认模式
为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(message acknowledgment)。接收者消息确认指的是否要将数据从队列中进行移除,如果确认消息则是将这条消息从队列中彻底移除掉。如果这条消息被成功处理(例如完成数据库的插入等等),这条消息才能被确认删除,如果没有被成功处理(例如服务崩溃),我们在队列中的消息不应该被确认移除
在声明接收消息时(channel.basicConsume),可以指定 autoAck
参数,当 autoAck
为false
时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则(autoAck=true
),消息被消费后会在队列中立即删除它,不管消息是否被接收到。
在Consumer中Confirm模式中分为手动确认和自动确认(autoAck=true)。
手动确认主要并使用以下方法:
-
basicAck
:用于肯定确认//deliveryTage:消息的编号,由RabbitMQ提供 //multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。 channel.basicAck(long deliveryTag, boolean multiple);
-
basicRecover
:路由不成功的消息可以使用recover重新发送到队列中。//requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃 channel.basicRecover(boolean requeue);
-
basicReject
:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。//deliveryTage:消息的编号,由RabbitMQ提供 //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃 channel.basicReject(long deliveryTag, boolean requeue);
-
basicNack
:可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true。//deliveryTage:消息的编号,由RabbitMQ提供 //multiple:true表示开启批量处理 //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃 channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
-
当程序执行中断或者因为网络原因,RabbitMQ 没有收到 ack,则也会将消息重新入队。
完整的程序:
channel.basicConsume("confirmDirectQueue", false, "", new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(message);
//获取消息在队列中的唯一标识
long messageNo = envelope.getDeliveryTag();
//根据消息的编号来确认消息,确认以后则表示这个消息已经全部完成处理
//进行消息确认,需要将这个消息从队列中移除掉
channel.basicAck(messageNo, true);
}
});
(4)事务与确认模式混用
强烈建议只使用消息确认模式,因为事务开销太大,假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,那么RabbitMQ会产生什么样的变化?
结果分为两种情况:
- autoAck=false手动确认的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到,但在确认消息会等到事务提交之后,如果你手动确认现在之后,又回滚了事务,那么会以事务回滚为主,此条消息会重新放回队列;
- autoAck=true如果自定确认为true的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了;
注意:如果两者都使用了的话,如果确认模式中使用的是异步的方法,则事务提交不能放在主线程中,因为主线程运行完后,确认模式的子线程可能还在运行,如果事务提交放在主线程中的话,则主线程执行完后,子线程中确认模式就无法进行事务提交了。故事务提交应放在模式确认的子线程中
二、SpringBoot集成RabbitMQ
1. 添加依赖
<!--spring集成amqp的起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--这个是测试的-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
2. 配置RabbitMQ
(1)通用配置
# 配置 rabbitmq 的ip
spring.rabbitmq.host=192.168.29.128
# 配置 rabbitmq 的端口
spring.rabbitmq.port=5672
# 配置 rabbitmq 的用户名
spring.rabbitmq.username=root
# 配置 rabbitmq 的密码
spring.rabbitmq.password=123
# 配置虚拟主机
spring.rabbitmq.virtual-host=/
# 配置连接超时时间
spring.rabbitmq.connection-timeout=15000
如果rabbitmq是集群的,则使用 addresses 来替换 host 和 port 配置如下:
#配置RabbitMQ的集群访问地址
spring.rabbitmq.addresses=192.168.222.129:5672,192.168.222.130:5672
(2)配置Producer
#开启发布者确认确认机制,snone为不启用,correlated是发布消息成功到交换器后会触发回调方法
spring.rabbitmq.publisher-confirm-type=correlated
#消息从交换机抵达队列确认,当消息没有路由从交换机到队列时,会进行回调
spring.rabbitmq.publisher-returns=true
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true
(3)配置Consumer
首先配置手工确认模式(默认是自动的),用于 ACK 的手工处理,这样我们可以保证消息的可靠性送达,或者在消费端消费失败的时候可以做到重回队列、根据业务记录日志等处理。我们也可以设置消费端的监听个数和最大个数,用于控制消费端的并发情况。我们要开启限流,指定每次处理消息最多只能处理两条消息。
#设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费之最大数量
spring.rabbitmq.listener.simple.max-concurrency=10
#在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
spring.rabbitmq.listener.simple.prefetch=2
3. 创建队列与交换机
- 创建交换机(Exchange):
- 直接new对应类型的交换机:DirectExchange,FanoutExchange,TopicExchange,并指定交换机的名字
- 通过交换机构造器对象创建各种类型的交换机:ExchangeBuilder.directExchange().build() 等
- 通过
AmqpAdmin
(在RabbitAutoConfiguration
中已经自动注入) 来创建
- 创建队列(Queue):
- 直接new一个Queue对象(注意包名是:org.springframework.amqp.core.Queue),并指定队列的名字
- 通过
AmqpAdmin(在
RabbitAutoConfiguration` 中已经自动注入) 来创建
- 创建队列与交换机的绑定对象(Binding)
- 通过BindingBuilder对象来进行绑定并指定RoutingKey
(1)直接创建
注意:
- 如果 RabbitMQ 中已经存在了相应的队列,交换机等,则需要先删除。例如,当第一次运行创建了某个 Queue 后,发现其中某个配置错了,改了相应的参数后,直接启动并不会覆盖原来的参数,需要在 RabbitMQ 中将之前创建的 Queue 删除后重新创建
- 并不是项目启动时就会直接创建配置中的队列,而是当第一次连接该队列时才会进行创建,例如有消费者监听这个端口时,交换机也一样。
//@Configuration 标记当前类是Spring的一个配置类,用于模拟Spring的xml配置文件
@Configuration
public class AmqpConfig {
//标记当前方法是一个Spring的Bean标签配置,方法名相当于bean标签的id 返回值相当于bean标签的class
//作用是用于创建一个对象到Spring的容器中
@Bean
public DirectExchange directExchange(){
/*
* String name, 交换机名字
* boolean durable, 是否持久化
* boolean autoDelete, 是否自动删除
* Map<String, Object> arguments 参数
*/
return new DirectExchange("BootDirectExchange", true, false, null);
}
@Bean
public Queue directQueue(){
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
return new Queue("BootDirectQueue", true, false, false);
}
//将队列与交换机进行绑定,并指定RoutingKey
//参数 1 为需要绑定的队列对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
//参数 2 为需要绑定的交换机对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
@Bean
public Binding directBinding(Queue directQueue, DirectExchange directExchange){
return BindingBuilder.bind(directQueue).to(directExchange).with("BootDirectRoutingKey");
}
//第二种创建 Binding 方式,与上一个方法结果相同
@Bean
public Binding directBinding2() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange, 交换机
* String routingKey, 路由键
* Map<String, Object> arguments 参数
*/
return new Binding("BootDirectQueue",
Binding.DestinationType.QUEUE,
"BootDirectExchange",
"BootDirectRoutingKey",
null);
}
/**
* 创建死信交换机,跟普通交换机一样,只是死信交换机只用来接收过期的消息
*/
@Bean
public DirectExchange deadExchange() {
return new DirectExchange("deadExchange", true, false);
}
/**
* 创建死信队列,该队列没有消费者,消息会设置过期时间,消息过期后会发送到死信交换机,在由死信交换机转发至处理该消息的队列中
*/
@Bean
public Queue DeadQueue() {
Map<String, Object> arguments = new HashMap<>();
// 死信路由到死信交换器DLX
arguments.put("x-dead-letter-exchange", "deadExchange");
arguments.put("x-dead-letter-routing-key", "deadRoutingKey"); //路由键
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
return new Queue("deadQueue", true, false, false, arguments);
}
}
(2)使用 AmqpAdmin 创建
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
private AmqpAdmin amqpAdmin;
@Test
void contextLoads() {
//创建交换机
amqpAdmin.declareExchange(new DirectExchange("hello-java-exchange", //名字
true, //是否持久化存储
false)); //是否自动删除
//创建队列,注意这里的 Queue 是org.springframework.amqp.core包下的
amqpAdmin.declareQueue(new Queue("hello-java-queue", //队列名
true, //是否持久化存储
false, //是否独占
false)); //是否自动删除
//创建绑定关系
amqpAdmin.declareBinding(new Binding("hello-java-queue", //目的地
Binding.DestinationType.QUEUE, //目的地类型(队列或者交换机)
"hello-java-exchange", //交换机
"hello.java", //路由键
null)); //自定义参数
}
}
4. AmqpTemplate发送消息
AmqpTemplate 它提供了通用的操作基于Amqp开发的消息队列的方法。同样我们需要进行注入到 Spring 容器中,然后直接使用。AmqpTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合时,在配置文件里添加配置即可。
-
获取AmqpTemplate对象,在Springboot中,在需要使用的类中直接获取:
@Autowired private AmqpTemplate amqpTemplate;
-
将java对象转换为Message对象,并发送到RabbitMQ
amqpTemplate.convertAndSend(String exchange, String routingKey, Object message)
- exchange:交换机名称
- routingKey:路由键
- message:消息
-
将Message消息转换为java对象
amqpTemplate.receiveAndConvert(String queueName)
- queueName:队列名字
- 返回值为Object,需要类型强转
5. RabbitTemplate发送消息
RabbitTemplate 即消息模板,RabbitTemplate 是 AmqpTemplate 接口的一个实现类,它除了提供了 AmqpTemplate
通用的方法外,还提供了针对RabbitMQ操作的方法,比如回调监听消息接口 ConfirmCallback、返回值确认接口 ReturnCallback 等等。
(1)发送消息
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void sendMessage() {
Map<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 18);
rabbitTemplate.convertAndSend("hello-java-exchange", //交换机
"hello.java", //路由键
map, //消息
new CorrelationData(UUID.randomUUID().toString())); //唯一Id
}
}
(2)设置发送回调
发送者确认模式,需要在发送消息之前进行设置:
@Configuration
public class MyRabbitConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
//使用JSON作为消息的序列化方式
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 定制rabbitTemplate
* 配置文件中需配置 spring.rabbitmq.publisher-confirm-type=correlated
*/
@PostConstruct //MyRabbitConfig 对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
//设置消息抵达交换机确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 当前消息的唯一关联数据(消息的唯一id)
* @param ack 消息是否成功收到,只要消息抵达broker,就是true
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback ==> correlationData[" + correlationData + "]==>ack[" + ack + "]==>cause[" + cause);
if (ack) {
System.out.println("消息发送确认成功");
} else {
System.out.println("消息发送失败:" + cause);
}
}
});
/**
* 设置消息抵达队列确认回调,配置文件中需要配:
* spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
*/
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("ReturnCallback ==> message[" + message + "]==>replyCode[" + replyCode + "]==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey);
//在这里可以对消息进行重发
}
});
}
}
关于确认与回调:
- 如果消息没有到
exchange
,则ConfirmCallback
回调,ack=false
- 如果消息到达
exchange
,则ConfirmCallback
回调,ack=true
exchange
到queue
成功,则不回调return
exchange
到queue
失败,则回调return
6. Consumer接收消息(@RabbitListener)
(1)使用@RabbitListener监听消息
@RabbitListener
:可以标在类上(要和 @RabbitHandler
配合使用)或者方法上,参数 queues
可以指定一个 String[]
,来监听多个队列。(类必须注入到 Spring 容器中)
- 方法的参数列表可以写的类型:
Message message
:原生消息详细信息,头 + 体T
:发送的消息的类型,比如发送消息时发了个 Map,则接收该消息时也可以使用 Map,Channel channel
:当前传输数据的管道
@Service
public class TestServiceImpl{
@RabbitListener(queues = "hello-java-queue")
public void directReceive(Message message, Map<String, Object> body, Channel channel) {
System.out.println("收到消息:" + body);
}
//也可以直接在注解中创建队列,交换机,然后指定routingKey进行绑定,监听
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "queue2", durable = "true"),
exchange = @Exchange(value = "exchange2",
type = "direct",
durable = "true", i
gnoreDeclarationExceptions = "true"
),
key = "routingKey2"
)
)
public void directReceive2(Message message, Channel channel) {
//这里是对取出来的message进行处理
}
}
(2)使用 @RabbitHandler 重载消息类型
@RabbitHandler
:当 @RabbitListener
标在类上时,要使用 @RabbitHandler
标在方法上。这样的好处是,当一个队列中传入了不同类型的对象时,可以通过重载的方式在方法形参中直接接收不同类型的消息
@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {
//消息类型为 Map
@RabbitHandler
public void directReceive(Map<String, Object> body) {
System.out.println("收到消息Map:" + body);
}
//消息类型为 String
@RabbitHandler
public void directReceive(String body) {
System.out.println("收到消息String:" + body);
}
}
(3)消息确认机制
要在配置文件中配置手动 ack:spring.rabbitmq.listener.simple.acknowledge-mode=manual
@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {
//消息类型为 Map
@RabbitHandler
public void directReceive(Message message, Map<String, Object> body, Channel channel) {
System.out.println("收到消息Map:" + body);
//channel中按顺序自增,所以是唯一的
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (deliveryTag % 2 == 0) {
/*
* 肯定确认,表明已经成功消费消息
* deliveryTage:消息的编号,由RabbitMQ提供
* multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
*/
channel.basicAck(deliveryTag, false);
} else {
/**
* 拒绝消息,表明消息消费失败
*deliveryTage:消息的编号,由RabbitMQ提供
*multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
* requeuq:true 表示重新入队;false 表示丢弃
*/
channel.basicNack(deliveryTag, false, true);
}
} catch (IOException e) {
//网络中断
e.printStackTrace();
}
}
}
7. 使用JSON的序列化方式
所有的序列化方式都实现了 MessageConverter
接口,默认使用的是 SimpleMessageConverter
,其使用 ObjectOutputStream
进行序列化,需要对象实现 Serializable
接口。
若将消息序列化为 json,则只需将自带的 Jackson2JsonMessageConverter
注入容器即可
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
三、常见问题及解决思路
1. 消息丢失
(1)消息发送出去,由于网络问题没有抵达服务器
- 做好容错方法(try - catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
- 做好日志记录,每个消息状态是否都被服务器收到都应该记录
- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
数据库消息记录样表:
CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL,
`content` text,
`to_exchane` varchar(255) DEFAULT NULL,
`routing_key` varchar(255) DEFAULT NULL,
`class_type` varchar(255) DEFAULT NULL,
`message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
(2)消息抵达 Broker,Broker 要将消息写入磁盘(持久化)才算成功,此时 Broker 尚未持久化完成,宕机
消息生产者也必须加入确认回调机制,确认成功的消息,修改数据库消息状态
(3)自动 ACK 的状态下,消费者收到消息,但没来得及消费然后宕机
一定开启手动 ACK,消费成功才移除,失败或者没来得及处理就 noAck 并重新入队
2. 消息积压
- 消费者宕机积压
- 消费者消费能力不足积压
- 发送者发送流量太大
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
3. 消息重复
- 消息消费成功,事务已经提交,ack 时,机器宕机,导致没有 ack 成功,Broker 的消息重新由 unack 变为 ready,并发送给其他消费者
- 消息消费失败,由于重试机制,自动又将消息发送出去
- 成功消费,ack 时宕机,消息由 unack 变为 ready,Broker 又重新发送
-
消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
-
使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
-
rabbitMQ 的每一个消息都有 redelivered 字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的。
//当前消息是否被第二次及以后(重新)派发过来了 Boolean redelivered = message.getMessageProperties().getRedelivered();
-
讲解比较深,讲的也非常棒:Java SpringBoot集成RabbitMq实战和总结
参考了这一篇博客:https://www.cnblogs.com/haixiang/p/10959551.html