了解熟悉RabbitMQ的高级特性
分别添加如下依赖【或者将依赖放置在父工程下,两个module作为子工程引用即可】
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
spring:
rabbitmq:
host: 服务器IP
port: 5672 # 端口默认为 5672
username: guest # 默认账号有guest 密码一致
password: guest
virtual-host: /
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
public static final String QUEUE_NAME = "csnz_queue";
public static final String EXCHANGE_NAME = "csnz_exchange";
// 1、注册队列
@Bean("CSNZQueue")
public Queue getQueue(){
// 使用QueueBuilder构建一个队列,设置队列持久化,以及自动删除。
return QueueBuilder.durable(QUEUE_NAME).autoDelete().build();
}
// 2、注册交换机
@Bean("CSNZExchange")
public Exchange getExchange(){
// 使用ExchangeBuilder构建一个交换机(类型可选,此处为通配符交换机),设置持久化和自动删除
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).autoDelete().durable(true).build();
}
// 3、绑定队列和交换机
@Bean("CSNZBind")
public Binding bindQueueExchange(@Qualifier("CSNZQueue")Queue queue,@Qualifier("CSNZExchange")Exchange exchange){
// 使用BindingBuilder 将刚刚声明的队列和交换机绑定并设置绑定的路由key
return BindingBuilder.bind(queue).to(exchange).with("csnz.#").noargs();
}
}
投递
(生产者发送的)可靠性确认模式:
1、在配置中开启 publisher-confirms 为 true
2、在rabbitTemplate定义 confirmCallBack 回调函数
/*
确认模式:
1、在配置中开启 publisher-confirms为true
2、在rabbitTemplate定义confirmCallBack回调函数
*/
@Test
public void testConfirm(){
// 定义confirmCallBack回调函数
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
/*
CorrelationData:相关的配置信息【在convertAndSend重载方法中有包含此信息】
ack;exchange交换机,是否成功收到了信息。
cause:失败原因。如果成功接收则此值为null
*/
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息成功发送");
}else{
System.out.println("发送失败原因:" + cause);
// 重新发起或其他操作
}
}
});
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testConfirm","测试确认回调模式");
}
此时,如果发送消息时指定的交换机和路由键都是正确的,即代码块第27行参数正确,则第17行的 ack值为true,执行 消息成功发送到交换机后的逻辑代码
这里发送失败就是因为我们把交换机的名称写错了,换成正确的交换机名称就好
回退模式:
当消息发送给Exchange后,Exchange路由到queue失败时才会执行 ReturnCallBack
1、在配置中开启 publisher-returns 为 true
2、设置ReturnCallBack
3、设置Exchange处理消息的模式:
如果消息没路由到queue
- 1、丢弃消息(默认)即 rabbitTemplate.setMandatory(false);
- 2、 返回给消息发送方 ReturnCallBack 即 rabbitTemplate.setMandatory(true);
@Test
public void testReturn(){
// 设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
// 设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
/*
Message:消息对象
replyCode:错误码
replyText:错误信息
exchange:交换机
routingKey:路由键
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(message);
}
});
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"error.testReturn","测试回退模式");
}
此时,如果发送消息时指定的交换机和路由键发生找不到的情况,即代码块第20行参数错误
情况一:没写第四行设置交换机处理失败消息的模式为true,则不会执行第15行的代码,因为消息失败默认是丢弃模式
因为默认此值就是false
情况二:设置交换机处理失败消息的模式为true,则会执行第15行的代码块,消息发送失败时,消息会通过回调返回,此时就可以查看消息发送失败的具体原因
消费者
收到消息后的确认模式。一般不会使用 自动确认模式,因为收到消息后,很可能在进行业务处理时出现异常,造成数据丢失。真就啪一下没了。
一般都是使用 手动确认模式
自动确认模式:
1、定义一个监听器:AckListener 实现 MessageListener 接口
2、在onMessage方法上绑定要监听的队列
@Component
public class AckListener implements MessageListener {
@Override
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void onMessage(Message message) throws Exception {
System.out.println(new String(message.getBody()));
}
}
手动确认模式:
1、设置手动接收:acknowledge-mode: manual
2、定义一个监听器:AckListener 实现 ChannelAwareMessageListener 接口 :(因为此接口才有返回channel参数)
3、在onMessage方法上绑定要监听的队列
4、消息成功处理:调用 channel.basicAck() 接收
5、消息处理失败:调用channel.basicNack()拒绝接收,让broker重新发送给consumer
# 配置RabbitMQ 的基本信息 IP 端口 username pass
spring:
rabbitmq:
host: 服务器IP
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual
@Component
public class AckListener implements ChannelAwareMessageListener { // 自动接收确认实现他即可 MessageListener
@Override
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(1000); // 模拟业务时间
// 传递标签:该字段为MQ server 用于消息确认的标记
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1、接收转换消息
System.out.println(new String(message.getBody()));
// 2、处理业务逻辑
Thread.sleep(1000); // 模拟业务时间
// 3、没问题的话进行手动接收:basicAck(long deliveryTag, boolean multiple)
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
// 4、有问题的话拒接接收:basicNack(long deliveryTag, boolean multiple, boolean requeue)
// ->requeue:是否重回队列,如果true,则消息重新回到queue,broker会重新发送该消息给消费端
channel.basicNack(deliveryTag,true,true);
}
}
}
@Component
public class LimitListener implements ChannelAwareMessageListener { // 自动接收确认实现他即可 MessageListener
@Override
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(1000); // 模拟业务时间
// 传递标签:该字段为MQ server 用于消息确认的标记
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1、接收转换消息
System.out.println(new String(message.getBody()));
// 2、处理业务逻辑
Thread.sleep(1000); // 模拟业务时间
// int i = 1/0;
// 3、没问题的话进行手动接收:basicAck(long deliveryTag, boolean multiple)
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
// 4、有问题的话拒接接收:basicNack(long deliveryTag, boolean multiple, boolean requeue)
// ->requeue:是否重回队列,如果true,则消息重新回到queue,broker会重新发送该消息给消费端
channel.basicNack(deliveryTag,true,true);
}
}
}
# 配置RabbitMQ 的基本信息 IP 端口 username pass
spring:
rabbitmq:
host: 服务器IP
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual
direct:
prefetch: 1
@Test
public void testSend(){
// 设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
// 设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
/*
Message:消息对象
replyCode:错误码
replyText:错误信息
exchange:交换机
routingKey:路由键
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(message);
}
});
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testSend","测试限流-手动接收:"+i);
}
}
@Test
public void testLimitAck(){
System.out.println("执行 限流Ack模式");
while(true){
}
}
且RabbitMQ不保证消息会在精确的TTL时间后立即被删除。这是因为RabbitMQ使用一种基于时间戳的方式来检查消息的过期时间,并且该方式是有一定的误差的
答:
A 消息会先过期,而不是队列中所有消息一起过期。因为 RabbitMQ 是按照消息的过期时间来进行消息的清理的。当 A 消息过期时,它会被从队列中删除,而不受队列总体消息过期时间的影响。队列总体消息过期时间只会影响那些没有设置过期时间的消息。因此,A 消息的过期时间不会被设置的总体过期时间所覆盖。
答:
A 消息会比队列中的其他消息后过期。因为 RabbitMQ 是按照消息的过期时间来进行消息的清理的。当队列中的消息的过期时间早于 A 消息的过期时间时,这些消息会先被删除,而 A 消息会继续存在于队列中,直到其过期时间到达后才会被删除。因此,A 消息会比队列中的其他消息后过期。
答:
设置队列过期时间,是指在队列空闲一段时间之后(即没有消费者消费该队列,也没有新消息进入该队列),队列会自动被删除。这个过期时间是应用于整个队列的,而不是具体某一条消息。
设置队列消息过期时间,是指在每一条消息入队时,设置消息的过期时间,当消息在队列中等待时间超过其过期时间时,该消息会被自动删除。这个过期时间是应用于具体某一条消息的,而不是整个队列。
设置队列消息过期使用x-message-ttl
参数,而设置队列过期使用x-expires
参数
在实际应用中,根据不同的需求,我们可以选择设置队列过期时间或设置队列消息过期时间。如果我们希望在一段时间内没有消费者消费该队列时,自动删除该队列,那么可以设置队列过期时间。如果我们希望在一条消息在队列中存活的时间超过一定时间后自动被删除,那么可以设置队列消息过期时间。
@Test
public void testOneTtl(){
// 消息后置处理器,可以设置一些参数
MessagePostProcessor processor = new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置message的信息
message.getMessageProperties().setExpiration("5000");
return message;
}
};
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testTTL","测试TTL过期:",processor);
}
在这里新增了一个args参数,作用是声明队列消息TTL以及过期时间
// 1、注册队列
@Bean("CSNZQueue")
public Queue getQueue(){
// 设置过期时间参数
HashMap<String, Object> args = new HashMap<>();
args.put("x-message-ttl",5000);
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
@Test
public void testTtl(){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testTTL","测试TTL过期:"+i);
}
}
我知道你们英语四级没过,把翻译也丢过来了:
如果消息被重新排队(例如,由于使用了具有requeue参数的AMQP方法,或者由于通道关闭),则保留消息的原始到期时间。
死信息
后,可以被重新发送到另一个交换机,此时另一个交换机就称之为死信交换机(DLX)。来自官方提示:Note that expiration of a queue will not dead letter the messages in it.
请注意,队列到期不会使其中的消息成为死信。(是队列到期,而不是队列消息到期)
To set the dead letter exchange for a queue, specify the optional x-dead-letter-exchange argument when declaring the queue. The value must be an exchange name in the same virtual host:
要为队列设置死信交换,请在声明队列时指定可选的x-dead-letter-exchange参数。该值必须是同一虚拟主机中的exchange名称
You may also specify a routing key to be used when dead-lettering messages. If this is not set, the message’s own routing keys will be used.
您还可以指定在死信消息时使用的路由关键字。如果没有设置,将使用消息自己的路由关键字 args.put(“x-dead-letter-routing-key”, “some-routing-key”);
队列设置参数:
@Bean("DLXQueue")
public Queue DLXQueue(){
return QueueBuilder.durable(DLX_QUEUE_NAME).autoDelete().build();
}
@Bean("DLXExchange")
public Exchange DLXExchange(){
return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).autoDelete().durable(true).build();
}
@Bean
public Binding bindDLX(){
return BindingBuilder.bind(DLXQueue()).to(DLXExchange()).with("DLX.#").noargs();
}
@Bean("CSNZQueue")
public Queue getQueue(){
HashMap<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange",DLX_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key","DLX.#");
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
@Test
public void testDLX(){
// 消息后置处理器,可以设置一些参数
MessagePostProcessor processor = new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置message的信息
message.getMessageProperties().setExpiration("10000");
return message;
}
};
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testDLX","测试DLX", processor);
}
队列设置参数:
@Bean("CSNZQueue")
public Queue getQueue(){
// 设置过期时间参数
HashMap<String, Object> args = new HashMap<>();
args.put("x-message-ttl",5000);
args.put("x-dead-letter-exchange",DLX_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key","DLX.#");
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
队列设置参数:
@Bean("CSNZQueue")
public Queue getQueue(){
// 设置过期时间参数
HashMap<String, Object> args = new HashMap<>();
args.put("x-max-length",5);
args.put("x-dead-letter-exchange",DLX_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key","DLX.#");
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
队列设置参数:
@Bean("CSNZQueue")
public Queue getQueue(){
// 设置过期时间参数
HashMap<String, Object> args = new HashMap<>();
args.put("x-expires",20000);
args.put("x-dead-letter-exchange",DLX_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key","DLX.#");
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
// 2、注册交换机
@Bean("CSNZExchange")
public Exchange getExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).autoDelete().durable(true).build();
// return ExchangeBuilder.directExchange(EXCHANGE_NAME).autoDelete().durable(true).build();
}
// 3、绑定队列和交换机
@Bean("CSNZBind")
public Binding bindQueueExchange(@Qualifier("CSNZQueue")Queue queue,@Qualifier("CSNZExchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("Delay.#").noargs();
}
/*
以下声明 死信交换机
*/
@Bean("DLXQueue")
public Queue DLXQueue(){
return QueueBuilder.durable(DLX_QUEUE_NAME).autoDelete().build();
}
@Bean("DLXExchange")
public Exchange DLXExchange(){
return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).autoDelete().durable(true).build();
}
@Bean
public Binding bindDLX(){
return BindingBuilder.bind(DLXQueue()).to(DLXExchange()).with("DLX.#").noargs();
}
@Test
public void testDelayMessageWithExpires(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"Delay.testDelay","测试队列过期是否其中的信息会成为死信",message -> {
return message;
});
}
相关场景:某多多下单后,30分钟内客户未支付,则自动取消此订单,库存回滚。
@Component
public class DelayListener implements ChannelAwareMessageListener { // 自动接收确认实现他即可 MessageListener
@Override
@RabbitListener(queues = RabbitMQConfig.DLX_QUEUE_NAME)
public void onMessage(Message message, Channel channel) throws Exception {
// 传递标签:该字段为MQ server 用于消息确认的标记
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1、接收转换消息
System.out.println(new String(message.getBody()));
// 2、处理业务逻辑
Thread.sleep(1000); // 模拟业务时间
// 3、没问题的话进行手动接收:basicAck(long deliveryTag, boolean multiple)
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
// 4、有问题的话拒接接收:basicNack(long deliveryTag, boolean multiple, boolean requeue)
// ->requeue:是否重回队列,如果true,则消息重新回到queue,broker会重新发送该消息给消费端
channel.basicNack(deliveryTag,true,true);
}
}
}
@Configuration
public class RabbitMQConfig {
public static final String QUEUE_NAME = "csnz_queue";
public static final String EXCHANGE_NAME = "csnz_exchange";
public static final String DLX_QUEUE_NAME = "DLX_QUEUE";
public static final String DLX_EXCHANGE_NAME = "DLX_EXCHANGE";
// 1、注册队列
@Bean("CSNZQueue")
public Queue getQueue(){
// 设置过期时间参数
HashMap<String, Object> args = new HashMap<>();
args.put("x-message-ttl",10000);
args.put("x-max-length",5);
args.put("x-dead-letter-exchange",DLX_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key","DLX.#");
return QueueBuilder.durable(QUEUE_NAME).withArguments(args).autoDelete().build();
}
// 2、注册交换机
@Bean("CSNZExchange")
public Exchange getExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).autoDelete().durable(true).build();
}
// 3、绑定队列和交换机
@Bean("CSNZBind")
public Binding bindQueueExchange(@Qualifier("CSNZQueue")Queue queue,@Qualifier("CSNZExchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("csnz.#").noargs();
}
/*
以下声明 死信交换机
*/
@Bean("DLXQueue")
public Queue DLXQueue(){
return QueueBuilder.durable(DLX_QUEUE_NAME).autoDelete().build();
}
@Bean("DLXExchange")
public Exchange DLXExchange(){
return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).autoDelete().durable(true).build();
}
@Bean
public Binding bindDLX(){
return BindingBuilder.bind(DLXQueue()).to(DLXExchange()).with("DLX.#").noargs();
}
}
生产者端:
@Test
public void testDelay() throws InterruptedException {
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"csnz.testDelay","测试延迟队列");
for (int i = 0; i < 15; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
消费者端:
@Test
public void testDelay(){
System.out.println("Delay模式");
while(true){
}
}
RabbitMQ延迟消息插件新增了一种新的交换器类型,消息通过这种交换器路由就可以实现延迟发送
插件官网下载:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases(记得适配版本号)
下载完成后将其解压在plugins文件夹下
运行cmd切换到rabbitMQ的sbin目录下执行:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启一下rabbitMQ服务
使用管理员运行CMD
去RabbitMQ的控制台看看,插件有没有加载成功
@Bean("DelayQueue")
public Queue DelayQueue(){
return QueueBuilder.durable(DELAY_QUEUE_NAME).autoDelete().build();
}
@Bean("DelayExchange")
public CustomExchange DelayExchange(){
HashMap<String, Object> args = new HashMap<>();
args.put("x-delayed-type","direct");
return new CustomExchange(DELAY_EXCHANGE_NAME," x-delayed-message",true,true,args);
}
@Bean
public Binding bindDelay(){
return BindingBuilder.bind(DelayQueue()).to(DelayExchange()).with("Delay.#").noargs();
}
@Test
public void testDelayMessage(){
rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE_NAME,"Delay.testDelay","测试延迟插件队列",message -> {
// setHeader 为延时时间 不填及为及时发送
message.getMessageProperties().setDelay(5000);
return message;
});
}
最终实现的效果就是发送的消息得等到5秒后才进入延迟队列,此时一步到位
此时需要注意的是,延迟队列插件的实现方式是通过在消息发布时设置消息的过期时间来实现的,因此在发送消息时,其实是MQ自动将消息的过期时间设置为当前时间加上延迟时间。
x-max-priority
参数设置优先级队列的最大值队列需要设置为优先级队列,消息需要设置优先级(在MQ出现消息堆积情况下、及消费速度小于生产速度,优先级才有意义)
x-max-priority
@Bean("DelayExchange")
public Exchange DelayExchange(){
HashMap<String, Object> args = new HashMap<>();
args.put("x-delayed-type","topic");
args.put("x-max-priority", 10);
return new CustomExchange(DELAY_EXCHANGE_NAME," x-delayed-message",true,true,args);
}
setPriority
级别 ,这里可以去掉设置优先级,然后多发几条,最后发现有设置优先级的消息最先被消费 @Test
public void testFirstMessage(){
rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE_NAME,"Delay.testFirst","测试延迟插件队列",message -> {
// setPriority 为设置此条数据的优先级
message.getMessageProperties().setPriority(5);
return message;
});
}