文章目录
上一篇:RabbitMQ安装与原理详解
官方文档:http://next.rabbitmq.com/api-guide.html
API文档:https://rabbitmq.github.io/rabbitmq-java-client/api/current/
一、使用Java实现消息的发送与接收
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
:用于肯定确认channel.basicAck(long deliveryTag, boolean multiple);
- deliveryTage:消息的编号,由RabbitMQ提供
- multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
-
basicRecover
:路由不成功的消息可以使用recover重新发送到队列中。channel.basicRecover(boolean requeue);
- requeue:true时将确认不成功的消息重新发送到队列中
-
basicReject
:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。channel.basicReject(long deliveryTag, boolean requeue);
-
basicNack
:可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true。channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
- multiple:true表示开启批量处理
完整的程序:
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
通用配置:
# 配置 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
配置发送端:
#支持消息发送失败返回队列
spring.rabbitmq.publisher-returns=true
#开启消息确认机制,simple为普通发送方确认模式,none为不启用
spring.rabbitmq.publisher-confirm-type=simple
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true
配置消费端:
首先配置手工确认模式,用于 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() 等
- 创建队列(Queue):
- 直接new一个Queue对象(注意包名是:org.springframework.amqp.core.Queue),并指定队列的名字
- 创建队列与交换机的绑定对象(Binding)
- 通过BindingBuilder对象来进行绑定并指定RoutingKey
//@Configuration 标记当前类是Spring的一个配置类,用于模拟Spring的xml配置文件
@Configuration
public class AmqpConfig {
//标记当前方法是一个Spring的Bean标签配置,方法名相当于bean标签的id 返回值相当于bean标签的class
//作用是用于创建一个对象到Spring的容器中
@Bean
public DirectExchange directExchange(){
//创建一个交换机 参数为交换机名
return new DirectExchange("BootDirectExchange");
}
@Bean
public Queue directQueue(){
//创建一个队列参数 为队列名称,其他参数设置参考普通Java代码创建队列的参数列表
return new Queue("BootDirectQueue");
}
//将队列与交换机进行绑定,并指定RoutingKey
//参数 1 为需要绑定的队列对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
//参数 2 为需要绑定的交换机对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
@Bean
public Binding directBinding(Queue directQueue, DirectExchange directExchange){
return BindingBuilder.bind(directQueue).to(directExchange).with("BootDirectRoutingKey");
}
/**
* 创建死信交换机,跟普通交换机一样,只是死信交换机只用来接收过期的消息
*/
@Bean
public DirectExchange deadExchange() {
return new DirectExchange("deadExchange", true, false);
}
/**
* 创建死信队列,该队列没有消费者,消息会设置过期时间,消息过期后会发送到死信交换机,在由死信交换机转发至处理该消息的队列中
*/
@Bean
public Queue BeadQueue() {
Map<String, Object> arguments = new HashMap<>(2);
// 死信路由到死信交换器DLX
arguments.put("x-dead-letter-exchange", "deadExchange");
arguments.put("x-dead-letter-routing-key", "deadRoutingKey");
return new Queue("deadQueue", true, false, false, arguments);
}
}
4. AmqpTemplate操作RabbitMQ
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,需要类型强转
-
持续监听队列,如果队列中有值,就取出来
- 在方法上使用
@RabbitListener
注解 - 参数
queues
指定监听哪个队列
@RabbitListener(queues = "myQueue") public void directReceive(String message, Channel channel) { //这里是对取出来的message进行处理 } //也可以直接在注解中创建队列,交换机,然后指定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 directReceive(String message, Channel channel) { //这里是对取出来的message进行处理 }
- 在方法上使用
5. RabbitTemplate操作RabbitMQ
RabbitTemplate 即消息模板,RabbitTemplate 是 AmqpTemplate 接口的一个实现类,它除了提供了AmqpTemplate通用的方法外,还提供了针对RabbitMQ操作的方法,比如回调监听消息接口 ConfirmCallback、返回值确认接口 ReturnCallback 等等。
发送者确认模式:
//发送者消息确认,在发送消息之前设置
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息发送确认成功");
} else {
System.out.println("消息发送失败" + cause);
}
}
});
//消息发送失败后,会将消息回传,在发送消息之前设置
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message,
int replyCode,
String replyText,
String exchange,
String routingKey) {
//在这里对消息进行重发
System.out.println("消息发送失败" + message.toString());
}
});
关于确认与回调:
-
如果消息没有到exchange,则confirm回调,ack=false
-
如果消息到达exchange,则confirm回调,ack=true
-
exchange到queue成功,则不回调return
-
exchange到queue失败,则回调return
接收者确认模式:
直接在消息接收者中通过接收到的Channen对象,进行消息确认即可,和之前讲的消费者消息确认一样(1.4.3)。
讲解比较深,讲的也非常棒:Java SpringBoot集成RabbitMq实战和总结
参考了这一篇博客:https://www.cnblogs.com/haixiang/p/10959551.html