1、引入spring-boot-starter-amqp的依赖,并配置host主机地址、port端口、virtualHost虚拟主机、用户名、密码等
2、声明交换机、队列、交换机与队列的绑定关系
3、使用RabbitTemplate的convertAndSend方法将消息发送给交换机,交换机收到消息后,路由给所绑定的队列
4、消费者使用@RabbitListener注解方法监听队列,当收到消息时,回调此注解方法
1、虚拟主机有什么用?
用于数据隔离,不同项目可以创建不同的虚拟主机。但是配置时,需要有对应虚拟主机权限的用户才可以使用指定的虚拟主机
用于数据隔离,不同项目可以创建不同的虚拟主机。但是配置时,需要有对应虚拟主机权限的用户才可以使用指定的虚拟主机
2、当在rabbitmq中已经在web管理后台,通过手动的方式创建了队列,交换机时,启动项目时又去声明队列,交换机时,然后项目停了,然后又去启动项目又去声明队列交换机时。或者后面启动项目时,声明的交换机类型改变时?原来已经存在的交换机或队列是否会被删除?原来里面已经存在的消息是否会被清空掉?
如果消息代理中已经存在对应名称的交换机,那么如果修改了代码中此交换机类型的定义,那么启动的时候就会报错。
3、rabbitTemplate的convertAndSend发送消息时,可以不指定交换机,直接发给队列(会使用默认的交换机,默认的交换机就是根据消息发送时所指定的routekey找到与此routeKey名称相同的消息队列)
4、rabbitTemplate的convertAndSend发送消息时,可以指定交换机,交换机会根据当前交换机类型和此交换机的绑定关系将消息路由给对应绑定的队列
5、rabbitTemplate的convertAndSend发送不同的java类型消息,可以指定消息转换器。在使用@RabbitListener注解方法监听消息时,声明发送的java类型即可
6、在同一项目中可以使用@RabbitmListener注解的1个方法来监听指定的多个消息队列。
7、在同一项目中的多个方法都使用了@RabbitListener注解,并且这几个方法监听的消息队列中有相同的,那么当这些相同消息队列中收到消息时,会负载均衡的交给这几个方法处理(也就是1个消息只会给到其中1个方法处理),这就是work queues工作队列模式。
8、在同一项目中的多个方法都使用了@RabbitListener注解,并且这几个方法监听的消息队列中有相同的,即使这几个方法中处理的效率有高有低(故意在其中某个方法中睡它10s,这个方法的处理效率就低了),但是他们收到的消息数量仍然是按负载均衡分发的。那么肯定要解决这个问题,因此可以加上配置:spring.rabbit.listener.simple.prefetch=1,意思就是消费者每次拉取1条消息,这条消息处理完成之后,消息代理才会将下1条消息发过来,这样就不是按照负载均衡的方式发给这多个方法了,而是能者多劳。
9、在不同的项目中都监听了同1个消息队列,本来这个队列中有消息时,是按负载均衡1个消费者1个来轮流发,但是如果其中某个消费者挂了,剩余的消息是否还按轮流来?当它再次上线,是否继续让它轮流来?
10、在同一队列上有多个消费者在监听,当消息发给某个消费者时,这个消费者在处理时发生了异常,这个消息会被忽略掉?还是交给其它消费者?默认会被忽略掉
11、@RabbitListener可以标注在方法上,指定需要监听的消息队列(可指定多个消息队列),然后在方法参数位置上声明所要处理的消息类型(消息的发送者所发送的类型)。
12、@RabbitListener可以标注在类上,然后在这个类中的某个方法上使用@RabbitHandler注解,并在这个 方法的方法参数位置声明byte[] data来接收消息数据。
13、1个消息队列有多个消费者在监听,每个消息都会只发给其中某1个消费者处理,这样的模型叫工作队列模式
14、交换机类型有默认交换机类型、Fanout类型、Direct类型、Topic类型。
Fanout类型交换机:1个Fanout类型交换机可以绑定多个消息队列,当Fanout类型收到1个消息时,会直接发给所绑定的每1个消息队列
Direct类型,1个Direct类型交换机可以绑定多个消息队列,每绑定1个消息队列时,需要指定对应的路由key(routeKey),当Direct类型交换机收到1个消息时,会根据消息发送时所指定的路由key发送给routeKey完全匹配到的消息队列,
Topic类型,1个Topic类型交换机可以绑定多个消息队列,每绑定1个消息队列时,需要指定对应的通配符(#代指0个或多个单词和*代指1个单词),当Topic类型交换机收到1个消息时,会根据消息发送时所指定的路由key发送给routeKey通配匹配到的消息队列,
15、Spring Amqp提供了声明队列、交换机、队列和交换机绑定关系的类。在sprigboot中如何使用呢?只需要将它们以bean的形式定义出来,并且项目中必须至少使用了1个@RabbitListener注解,那么springboot会帮助我们在rabbitmq消息代理服务器中创建对应的队列、交换机、队列和交换机绑定关系(注意前提:要想以@bean的方式创建队列和交换机,必须至少有一个监听者@RabbitListener,否则即使声明了Queue、Exchange、Binding这些bean,也不会创建成功的队列和交换机的)
声明代码示例
@Configuration
public class QueueConfig {
@Bean
public Queue queue3() {
return QueueBuilder.durable("direct.queue3").build();
}
@Bean
public Exchange exchange3() {
return ExchangeBuilder.directExchange("direct.exchange3").build();
}
@Bean
public Binding binding3() {
return BindingBuilder.bind(queue3()).to(exchange3()).with("A3").noargs();
}
}
16、Spring Amqp还可以使用@RabbitListener注解来声明队列和交换机(这个不需要前提,直接如下声明就会创建),如下声明会创建对应的消息队列,交换机,交换机和队列的绑定,并且当消息队列中有消息时,被注解的方法将会被回调
@RabbitListener(bindings = {
@QueueBinding(
// 队列是否持久: 当消息代理重启时, 非持旧队列将会干掉了。不设置时,默认是持久的。
value = @Queue(value = "direct.queue1",durable = "true"),
// 交换机是否持久: 当消息代理重启时, 非持久队列将会干掉了。
// 不设置时,默认是持久的。
// 默认就是DIRECT类型交换机
exchange = @Exchange(value = "direct.exchange1",type = ExchangeTypes.DIRECT),
// 当发送到direct.exchange1交换机的消息时,所指定的routeKey是red或者是blue时,由此方法处理
key = {"red","blue"}
)
})
public void listenQueue(AddUser addUser) {
// ...
}
17、使用rabbitmqTemplate#convert(exchange, routeKey, object)发送消息时,所发送消息的类型是Object类型。默认支持的类型是Message类型,它有2个属性byte[] body和MessageProperties messageProperties。
如果发送的消息的类型不是Message类型,那么会使用1个消息转换器(org.springframework.amqp.support.converter.MessageConverter),将object对象转换为Message对象。
在spring amqp中默认使用的是SimpleMessageConverter对此消息作序列化处理,它会在object是byte[]时,直接就创建Message,是String时,直接获取字符串转为utf8编码的字节,实现了Serializable时,使用jkd的序列化机制转为byte[],在这3种情况下,都会设置MessageProperties#setContentType为对应的内容类型。在rabbitmq的web后台管理页面看到的是字节数组转base64字符串的形式。在反序列化时,也会使用对应的逆向方式去作反序列化。
可以使用jackson序列化的方式,引入jackson的依赖com.fasterxml.jackson.core的jackson-databind,然后定义org.springframework.amqp.support.converter.Jackson2JsonMessageConverter的bean即可,它会自动生效的。也可以直接创建它,然后将它直接设置给RabbitTemplate就行了
18、docker安装rabbitmq
docker run \
-e RABBITMQ_DEFAULT_USER=guest \
-e RABBITMQ_DEFAULT_PASS=guest \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network hmall \
-d \
rabbitmq:3.8-management
19、消息可靠性,消息从发送者到mq,再从mq到消费者,其中每个环节都可能发生问题。
发送者的可靠性,这部分的可靠性由生产者重连机制和生产者确认机制来保证。
生产者重连:由于网络波动的存在,可能会出现客户端连接mq失败的情况。spring amqp提供了开启连接失败后的重连机制(注意:这不是消息发送失败的重试机制,是连接失败的重试机制)。但是,这个重连是阻塞式的重试,在多次重试等待的过程中,当前线程是被阻塞的,因此如果对业务性能有要求,建议禁用重试机制。如果一定要使用,就要合理配置等待时长和重试次数,当然,也可以考虑使用异步线程来执行发送消息的代码
(注意:这里说的重连是项目启动之后,使用rabbitmqTemplate发送消息时,肯定需要连接mq,是这个时候的重连,不是指的,项目启动时的重连。并且使用rabbitmqTemplate发送消息的下1行代码在重连期间是不会执行的,当在试完最大尝试次数还没有连接成功后,就会在当前线程抛出异常,下1行代码不会执行了)
spring:
rabbitmq:
host: xxx.xx.xx.xx
port: 5672
virtual-host: /demo-vh
username: guest
password: xxxxxx
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
生产者确认:rabbitmq提供了Publisher Confiirm和Publisher Return这2种确认机制。开启确认机制后,在mq成功收到消息后,会返回确认消息给生产者。返回的额结果有以下几种情况:
配置如下:
spring:
rabbitmq:
host: xxx.xx.xx.xx
port: 5672
virtual-host: /demo-vh
username: guest
password: xxxxxx
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启确认机制(注意开启确认机制后,对效率有所影响的哦,
# 演示发送大量消息时,建议关掉)
publisher-returns: true # 开启确认机制
设置confirmCallback和returnCallback
@Slf4j
@Configuration
public class RabbitConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@PostConstruct
public void postProcessTemplate() {
// 设置returnCallback
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/* 1. 当发送消息给mq的交换机, 并且指定1个在此交换机上不存在的绑定关系的routeKey时, 此方法会被回调
2. 当发送消息给mq的交换机, 并且交换机能够根据绑定关系将消息路由到队列时, 这个方法是不会回调的
*/
@Override
public void returnedMessage(Message message,
int replyCode,
String replyText,
String exchange,
String routingKey) {
log.info("统一收到returnedMessage, message: {}, replayCode:{}, replyText: {}," +
"exchange: {}, routeKey:{}",
message, replyCode, replyText, exchange, routingKey);
}
});
// 设置confirmCallback
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/* 1. 当发送消息给指定的交换机, mq交换机收到消息时, 会回调此方法, 并且传过来的ack为true
(无论此交换机后面是否能将此消息路由到队列, 都会回调此方法)
2. 当发送消息给1个不存在交换机时, 会回调此方法, 传过来的ack为false
3. 当发送1个消息时, 此时断网的情况下, 经过一小段时间后, 会回调此方法, 传过来的ack为false
*/
@Override
public void confirm(CorrelationData correlationData, // 能够从此对象中拿到发送消息时的信息
boolean ack,
String cause) {
log.info("统一收到回执, ack: {}, correlationData: {}, cause: {}",
ack, correlationData, cause);
}
});
log.info("已设置confirmCallback和returnCallback");
}
}
发送消息
@RequestMapping("rabbit")
@RestController
public class RabbitController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendToMq2")
public Object sendToMq2(String content, String targetExchage, String routeKey) {
HashMap<String, Object> data = new HashMap<>();
data.put("content", content);
rabbitTemplate.convertAndSend(targetExchage, routeKey, data);
return "ok";
}
@GetMapping("sendToMq3")
public Object sendToMq3(String content, String targetExchage, String routeKey) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(UUID.randomUUID().toString());
correlationData
.getFuture()
.addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
log.info("失败...");
}
/* 接收到回执时, 该方法触发。
*/
@Override
public void onSuccess(CorrelationData.Confirm result) {
log.info("接收到回执, 是否ack: {}, 原因: {}",
result.isAck(), result.getReason());
}
});
HashMap<String, Object> data = new HashMap<>();
data.put("content", content);
rabbitTemplate.convertAndSend(targetExchage, routeKey, data, correlationData);
return "ok";
}
}
上面我用的spring-boot-starter-amqp版本是2.1.8.RELEASE,在2.7.12版本中,配置有所不同,应如下配置:
spring:
rabbitmq:
publisher-confirm_type: correlated # 开启publisher confirm机制, 并设置confirm类型
# publisher-confirm-type有3中模式可选
# - none 关闭confirm机制
# - simple 同步阻塞等待mq的绘制消息
# - correlated 异步回调方式返回回执消息
publisher-returns: true # 开启publisher return机制
其中,当publisher-confirm_type: simple时,发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
mq的可靠性:在默认情况下,rabbitmq会将收到的消息保存到内存中以降低消息收发的延迟。这样会导致2个问题。1个是:一旦mq宕机,内存中的消息会丢失。第二个是:内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发mq阻塞。
数据持久化
LazyQueue:从RabbitMO的3.6.0版本开始,就增加了Lazy Queue的概念,也就是惰性队列。性能较之前有很大提升
惰性队列的特征如下
如何创建惰性队列
在rabbitmq后台管理页,在声明队列时,指定Aruguements参数中,添加x-queue-mode为lazy即可
代码的方式
@Bean
public Queue queue4() {
return QueueBuilder
.durable("direct.queue4")
// .lazy() // 需要2.2版本以上才有直接设置lazy的方法,不过没事,用下面的也是一样的
.withArgument("x-queue-mode","lazy")
.build();
}
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue2",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void lazyQueue2(String msg) {
log.info("消费消息: {}", msg);
}
rabbitmq如何保证消息的可靠性?
消费者的可靠性
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式(通过:spring.rabbitmq.listener.simple.acknowledge-mode属性来配置),有如下三种方式
none: 不处理。即消息投递给消费者后立刻ack,消息会立刻从Mq删除,不管监听方法是否出现异常。
manual: 手动模式。需要自己在业务代码中调用api,发送ack或reject,可以捕获异常控制重试次数,甚至可以控制失败消息的处理方式,存在业务入侵,但更灵活
代码示例
@Component
@RabbitListener(queues = "test.queue1")
public class MessageConsumer {
@RabbitHandler
public void recivedMessage(Message msg,
OrderReturnApplyEntity orderReturnApplyEntity,
Channel channel) throws IOException {
try {
System.out.println("接收到消息:" + msg);
int i = 1 / 0;
// 确认收到消息,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
if (msg.getMessageProperties().getRedelivered()) {
System.out.println("消息重试后依然失败,拒绝再次接收");
// 拒绝消息,不再重新入队
// (如果绑定了死信队列消息会进入死信队列,没有绑定死信队列则消息被丢弃,
// 也可以把失败消息记录到redis或者mysql中),也可以设置为true再重试。
channel.basicReject(msg.getMessageProperties().getDeliveryTag(), false);
} else {
System.out.println("消息消费时出现异常,即将再次返回队列处理");
// Nack消息,重新入队(重试一次)参数二表示是否批量,参数三表示是否重新入队列
channel.basicNack(msg.getMessageProperties().getDeliveryTag(),
false, true);
}
log.error("处理消息发生错误: {}", e);
}
}
}
配置如下:
server:
port: 8081
spring:
rabbitmq:
host: 119.23.61.24
port: 5672
virtual-host: /demo-vh
username: guest
password: 17E821zj
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启消息发送确认机制
publisher-returns: true # 开启消息return机制
listener:
simple:
prefetch: 1 # 每次拉取1个消息, 处理完成后, 再拉取下1个消息, 能者多劳
acknowledge-mode: munual # 消费者确认机制为手动
问题1:假设已经配置为manual手动确认模式,在recivedMessage方法中,忘记basicAck或者basicReject或者basicNack, 会怎么样?
测试步骤:在rabbitmq后台手动发1条数据,到test.queue1队列中,在recivedMessage处理消息的方法中,声明该消息,但就是不去(basicAck或者basicReject或者basicNack)。发现,消息处理方法收到了1次消息,因此方法只调用了1次,在rabbitmq的web后台该消息一直处于unacked状态。此时,关闭消费者服务,在rabbitmq的web后台该消息处于ready状态,即待投递。然后,再次启动消费者,此消息又投递了过来,消费者方法又调用了1次,此时再以同样的配置和代码启动另外1个消费者,这个新启动的消费者没有收到这个消息(说明它不会将已投递但未确认的消息投递给这个新的消费者)。然后将原来的消费者停掉,此时发现新启动的消费者立刻收到了这条消息,不过消息仍处于unacked状态。这证明这个消息在发送给1个消费者之后,会等待消费者的回执,如果消费者迟迟不给回执,那就一直等,直到这个消费者挂了,消息才会变为ready待投递状态,才会投递给其它的消费者。
测试2:使用basicAck确认收到消息后,消息将从队列中删除。如下代码测试,当收到消息时,使用basicAck(消息投递标记,是否批量确认),批量确认指的是,将deliveryTag小于当前消息投递标记的消息一并确认,这样broker就会清理掉之前未确认的消息,这可以适用于某些情况:既然最后面的消息都确认了,之前的消息确不确认也就没啥关系的情况。
@Slf4j
@Configuration
public class RabbitConfig {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange2",
type = ExchangeTypes.FANOUT),
key = {"red","blue"}
)
})
public void listenQueue(Message message, String msg, Channel channel) {
log.info("收到消息=====================");
log.info("channel:{}", channel);
log.info("msg:{}", msg);
log.info("message:{},", new String(message.getBody()));
// receivedDeliveryMode-是否持久化的消息,
// redelivered-是否重新投递的消息,
// receivedRoutingKey-路由key,
// deliveryTag-投递唯一标记(从1开始递增, 每次消费者重启后, 继续从1开始)
// consumerTag-当前消费者唯一标记(每个消费者都有自己的唯一标记,每次消费者重连后,生成新的标记)
// consumerQueue-当前消费者收到消息的队列
log.info("messageProperties:{}", message.getMessageProperties());
// deliveryTag-投递标记(broker用于标记此消息),
// multiple-是否批量确认(批量确认会让broker将小于当前消息的deliveryTag的消息给确认掉删了)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("处理结束=====================");
}
测试3:使用basicNack(deliveryTag, mulitiple, requeue) 拒绝签收该消息,第3个参数决定是否让消息重新回到队列,如果消息回到队列后,重新变为ready待投递状态,会选择消费者再次进行进行投递。如果不回到队列,那么broker将会删除此消息,但是如果此队列还绑定了死信交换机,那么此消息将会发给死信交换机。
@Slf4j
@Configuration
public class RabbitConfig {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange2",
type = ExchangeTypes.FANOUT),
key = {"red","blue"}
)
})
public void listenQueue(Message message, String msg, Channel channel) {
log.info("收到消息=====================");
log.info("channel:{}", channel);
log.info("msg:{}", msg);
log.info("message:{},", new String(message.getBody()));
// receivedDeliveryMode-是否持久化的消息,
// redelivered-是否重新投递的消息,
// receivedRoutingKey-路由key,
// deliveryTag-投递唯一标记(从1开始递增, 每次消费者重启后, 继续从1开始)
// consumerTag-当前消费者唯一标记(每个消费者都有自己的唯一标记,每次消费者重连后,生成新的标记)
// consumerQueue-当前消费者收到消息的队列
log.info("messageProperties:{}", message.getMessageProperties());
// deliveryTag-投递标记(broker用于标记此消息),
// multiple-是否批量确认(批量确认会让broker将小于当前消息的deliveryTag的消息给确认掉删了)
// requeue-是否继续入队,
// =====================以下是2种情况的代码及对应的解释=====================
// 如果不继续入队, 那么broker将会删除这个消息,
// 但是如果这个队列绑定了死信交换机,那么会发到该私信交换机中
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
// 如果继续入队, 那么消息重新回到队列处于待投递状态, 然后又会投递给当前消费者,
// 然后当前消费者又去让这个消息去入队待投递, 然后又投递给当前消费者,
// 然后就成了死循环了。
// 此时, 再开1个一样的消费者,再监听此队列,结果2个消费者都死循环了。
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
log.info("处理结束=====================");
}
}
测试4:使用basicReject(deliveryTag, requeue) 拒绝该消息,与上面使用basicNack(deliveryTag, multiple, requeue)一样的测试结果,只是basicNack方法中比basicReject多了个multiple的参数。
@Slf4j
@Configuration
public class RabbitConfig {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange2",
type = ExchangeTypes.FANOUT),
key = {"red","blue"}
)
})
public void listenQueue(Message message, String msg, Channel channel) {
log.info("收到消息=====================");
log.info("channel:{}", channel);
log.info("msg:{}", msg);
log.info("message:{},", new String(message.getBody()));
// receivedDeliveryMode-是否持久化的消息,
// redelivered-是否重新投递的消息,
// receivedRoutingKey-路由key,
// deliveryTag-投递唯一标记(从1开始递增, 每次消费者重启后, 继续从1开始)
// consumerTag-当前消费者唯一标记(每个消费者都有自己的唯一标记,每次消费者重连后,生成新的标记)
// consumerQueue-当前消费者收到消息的队列
log.info("messageProperties:{}", message.getMessageProperties());
// deliveryTag-投递标记(broker用于标记此消息),
// requeue-是否继续入队,
// =====================以下是2种情况的代码及对应的解释=====================
// 如果不继续入队, 那么broker将会删除这个消息,
// 但是如果这个队列绑定了死信交换机,那么会发到该私信交换机中
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
// 如果继续入队, 那么消息重新回到队列处于待投递状态, 然后又会投递给当前消费者,
// 然后当前消费者又去让这个消息去入队待投递, 然后又投递给当前消费者,
// 然后就成了死循环了。
// 此时, 再开1个一样的消费者,再监听此队列,结果2个消费者都死循环了。
// channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
log.info("处理结束=====================");
}
}
auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,
如果是业务异常,会自动返回nack,并重新入队(就会导致无限重试,导致程序死循环)
如果是消息处理或校验异常,自动返回reject,消息会被删除,不会重新入队,不会导致死循环(在监听方法中,手动抛出MessageConversionException,那么也是跟reject并且不重新入队,一样的效果,消息会被删除,不会导致死循环)
测试代码:
配置
server:
port: 8081
spring:
rabbitmq:
host: 119.23.61.24
port: 5672
virtual-host: /demo-vh
username: guest
password: 17E821zj
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启消息发送确认机制
publisher-returns: true # 开启消息return机制
listener:
simple:
prefetch: 1 # 每次拉取1个消息, 处理完成后, 再拉取下1个消息, 能者多劳
acknowledge-mode: auto # 消费者确认机制
代码:下面代码就是,故意在确认机制已经是自动确认的配置下,依然确认或拒绝,检查不同情况下,消息的流转情况
@Slf4j
@Configuration
public class RabbitConfig {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange2",
type = ExchangeTypes.FANOUT),
key = {"red","blue"}
)
})
public void listenQueue(Message message, String msg, Channel channel) {
log.info("收到消息=====================");
log.info("channel:{}", channel);
log.info("msg:{}", msg);
log.info("message:{},", new String(message.getBody()));
// receivedDeliveryMode-是否持久化的消息,
// redelivered-是否重新投递的消息,
// receivedRoutingKey-路由key,
// deliveryTag-投递唯一标记(从1开始递增, 每次消费者重启后, 继续从1开始)
// consumerTag-当前消费者唯一标记(每个消费者都有自己的唯一标记,每次消费者重连后,生成新的标记)
// consumerQueue-当前消费者收到消息的队列
log.info("messageProperties:{}", message.getMessageProperties());
String body = new String(message.getBody());
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// 以下的原意是指: 方法本身的作用, 并不是在确认模式是自动确认下调用这些方法的作用
if ("1".equals(body)) {
// 原意: 确认, 不批量
//(channel会shutdown, 然后会重新连接, 然后消息被删除)
channel.basicAck(deliveryTag, false);
}
else if ("2".equals(body)) {
// 原意: 拒绝, 重新入队
//(channel会shutdown, 然后会重新连接, 消息会重新入队,
// 然后消费者再次收到此消息, 然后不断循环, 并且中间的过程会抛出异常)
channel.basicReject(deliveryTag, true);
}
else if ("3".equals(body)) {
// 原意: 拒绝, 不重新入队(broker将会删除此消息,
// 如果该队列还绑定了死信交换机,那么会发往此交换机)
//(收到1次消息后, channel会shutdown, 然后会重新连接, 不会再次收到该消息,
// 因为消息已经被删除了)
channel.basicReject(deliveryTag, false);
}
else if ("4".equals(body)) {
// 原意: 拒绝, 不批量, 重新入队(会再次投递给消费者)
//(与2表现几乎一致)
channel.basicNack(deliveryTag, false, true);
}
else if ("5".equals(body)) {
// 原意: 拒绝, 批量, 重新入队(对于之前未确认的消息,批量拒绝并重新入队)
//(与2表现几乎一致)
channel.basicNack(deliveryTag, true, true);
}
else if ("6".equals(body)) {
// 抛出空指针异常,
//(收到1次消息, 然后这里抛出异常, 然后会自动nack并重新入队, 然后又收到该消息, 不断循环)
throw new NullPointerException("666...");
}
else if ("7".equals(body)) {
// 抛出消息转换异常,
//(抛出异常,消息会被删除, 并且不会重新入队, 不会死循环。
// 同basicReject拒绝消息并且设置不重新入队)
throw new MessageConversionException("777...");
}
else {
// 模拟正常处理
System.out.println("自动确认模式正常处理情况...");
}
log.info("处理结束=====================");
}
}
失败重试机制:
当消费者出现异常后,如果消费者设置的参数让此消息再次回到队列,那么消息会requeue(重新入队)到队列,等待投递,然后就会再投递给消费者,消费者收到该消息后再次异常,由于消费者设置的参数又让此消息再次回到队列,因此就会无限循环,导致mq的消息处理飙升,带来不必要的压力。我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列,尝试作如下配置来设置消费者。
server:
port: 8081
spring:
rabbitmq:
host: xx.xx.xx.xx
port: 5672
virtual-host: /demo-vh
username: guest
password: xx
connection-timeout: 1s # 设置mq的连接超时时间
template: #(消息生产者的配置)
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启消息发送确认机制
publisher-returns: true # 开启消息return机制
listener: # (消息消费者的配置)
simple:
prefetch: 1 # 每次拉取1个消息, 处理完成后, 再拉取下1个消息, 能者多劳
acknowledge-mode: auto # 消费者确认机制
## ===========添加失败重试机制===========
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1s
multiplier: 1 # 下次失败的等待时长倍数,
# 下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数(不设置的话, 默认配置的就是3次)
stateless: true # true无状态, false有状态。如果业务中包含事务, 这里改为false
当消费者添加如上失败重试机制前,我们发现本来消费者的监听方法抛出空指针异常,然后一直在不断的nack消息(并且设置重新入队参数为true),然后mq又投递过来然后又导致空指针异常,然后又nack消息并重新入队,然后不断的死循环的跑着(同上1个例子配置确认模式为auto,并且监听方法中抛出NullPointerException异常的例子)。加上失败重试机制的配置后,同样是在确认模式为auto,并且监听方法中抛出NullPointerException异常的情况下,发现消费者就拉取了1次消息,然后在本地重试了3次,在这期间mq也并没有投递消息过来,当重试3次都失败后,此消息从消息队列中删除了(重试次数耗尽都失败之后,直接拒绝了该消息,并且不重新入队)。
失败消息处理策略:在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现
实现方式分类
RepublishMessageRecoverer使用示例
示例描述:当向direct.queue2发送1个payload为7的消息时,消费者就会只拉取1次该消息,并且会在本地重试3次,如果重试3次都失败(本例中只要消息是7就会抛出空指针异常)之后,就会发送到error.direct交换机,然后根据路由key路由到error.queue消息队列,其中消息的内容就是异常栈的字符串,这样就可以让人工介入处理。并且使用失败消息处理策略后,不会出现无限制:失败重试,然后重发,继续失败重试。
server:
port: 8081
spring:
rabbitmq:
host: xx.xx.xx.xx
port: 5672
virtual-host: /demo-vh
username: guest
password: xxx
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启消息发送确认机制
publisher-returns: true # 开启消息return机制
listener:
simple:
prefetch: 1 # 每次拉取1个消息, 处理完成后, 再拉取下1个消息, 能者多劳
acknowledge-mode: auto # 消费者确认机制
## ===========添加失败重试机制===========
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1s
multiplier: 1 # 下次失败的等待时长倍数,
# 下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数(不设置的话, 默认配置的就是3次)
stateless: true # true无状态, false有状态。如果业务中包含事务, 这里改为false
@Slf4j
@Configuration
public class RabbitConfig {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange2",
type = ExchangeTypes.FANOUT),
key = {"red","blue"}
)
})
public void listenQueue(Message message, String msg, Channel channel) {
log.info("收到消息=====================");
log.info("channel:{}", channel);
log.info("msg:{}", msg);
log.info("message:{},", new String(message.getBody()));
// receivedDeliveryMode-是否持久化的消息,
// redelivered-是否重新投递的消息,
// receivedRoutingKey-路由key,
// deliveryTag-投递唯一标记(从1开始递增, 每次消费者重启后, 继续从1开始)
// consumerTag-当前消费者唯一标记(每个消费者都有自己的唯一标记,每次消费者重连后,生成新的标记)
// consumerQueue-当前消费者收到消息的队列
log.info("messageProperties:{}", message.getMessageProperties());
String body = new String(message.getBody());
long deliveryTag = message.getMessageProperties().getDeliveryTag();
if ("7".equals(body)) {
// 抛出消息转换异常,
//(抛出异常,消息会被删除, 并且不会重新入队, 不会死循环。
// 同basicReject拒绝消息并且设置不重新入队)
throw new MessageConversionException("777...");
}
log.info("处理结束=====================");
}
}
@Slf4j
@Configuration
// 当开启了消费者失败重试时, 当前配置类才生效
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled",
havingValue = "true")
public class ErrorConfiguration {
// 定义1个直连交换机
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.direct");
}
// 定义1个消息队列
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
// 绑定
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
// 消息消费时重试耗尽并且都失败(例如: 确认模式为auto,并且监听方法中抛出NullPointerException异常)时
// 的后续处理策略,
// 因为这里返回的是RepublishMessageRecoverer,所以在重试耗尽时发送到指定交换机,并携带指定的路由key
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
log.debug("加载RepublishMessageRecoverer");
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
业务幂等性:通过以上所有的手段,我们可以保证消息至少被消费者消费1次。但是由于网络波动等原因导致消费者消费同一消息多次,这个时候,就需要保证消息的幂等性。所谓的幂等性指的是,消费同一消息多次产生的效果与消费该消息1次的效果是相同的,或者说对业务状态的影响是一致的。
唯一消息id方案:
给每个消息都设置一个唯一id,利用id区分是否是重复消息:
使用步骤:消费者和生产者中都配置如下的消息转换器,并且设置createMessageIds属性为true
@Bean
public MessageConverter jacksonMessageConvertor(){
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 1. 设置此属性后, 会在使用rabbitTemplate发送消息时,
// 当未设置消息属性MessageProperties#messageId时,对消息对象的MessageProperties的messageId设
// 置1个uuid值, 用来作为这条消息的标识。
// 2. 当然也可以在使用rabbitTemplate发送消息时, 指定1个MessagePostProcessor,
// 来设置MessageProperties#messageId的值
jjmc.setCreateMessageIds(true);
return jjmc;
}
业务判断
结合业务逻辑,基于业务本身作判断
以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理:
@Component
@RequiredArgsConstructor
public class PayStatusListener {
private final IOrderService orderService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "mark.order.pay.queue", durable = "true"),
exchange = @Exchange(name = "pay.topic", type = ExchangeTypes.TOPIC),
key = "pay.success"
))
public void listenOrderPay(Long orderId) {
/*
// 1.查询订单
Order order = orderService.getById(orderId);
// 2.判断订单状态是否为未支付
if(order == null || order.getStatus() != 1){
// 订单不存在,或者状态异常
return;
}
// 3.如果未支付,标记订单状态为已支付
orderService.markOrderPaySuccess(orderId);
*/
// 其实可以使用下面的一步搞定(类似于乐观锁机制)
// update order set status = 2 where id = ? AND status = 1
orderService.lambdaUpdate()
.set(Order::getStatus, 2)
.set(Order::getPayTime, LocalDateTime.now())
.eq(Order::getId, orderId)
.eq(Order::getStatus, 1)
.update();
}
}
如何保证支付服务与交易服务之间的订单状态一致性?
如果交易服务消息处理失败,有没有什么兜底方案?
延迟消息:胜场这发送消息时,指定1个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息
死信交换机方案
示例
配置如下:
server:
port: 8081
spring:
rabbitmq:
host: xx.xx.xx.xx
port: 5672
virtual-host: /demo-vh
username: guest
password: xxx
connection-timeout: 1s # 设置mq的连接超时时间
template:
retry:
enabled: true # 开启超时重连机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重连次数
publisher-confirms: true # 开启消息发送确认机制
publisher-returns: true # 开启消息return机制
listener:
simple:
prefetch: 1 # 每次拉取1个消息, 处理完成后, 再拉取下1个消息, 能者多劳
acknowledge-mode: auto # 消费者确认机制
## ===========添加失败重试机制===========
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1s
multiplier: 1 # 下次失败的等待时长倍数,
# 下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数(不设置的话, 默认配置的就是3次)
stateless: true # true无状态, false有状态。如果业务中包含事务, 这里改为false
代码如下
@Slf4j
@Configuration
public class RabbitConfig {
/* 死信处理的交换机、队列、绑定等定义 */
@Bean
public org.springframework.amqp.core.Exchange dlxExchange() {
Exchange exchange = ExchangeBuilder.directExchange("dlx.directExchange")
.durable(true)
.build();
return exchange;
}
@Bean
public org.springframework.amqp.core.Queue dlxQueue() {
Queue queue = QueueBuilder.durable("dlx.queue").build();
return queue;
}
@Bean
public Binding dlxExAndQueueBinding() {
// 建立 dlx.directExchange交换机 到 dlx.queue队列 的绑定关系, 并指定路由key为red
Binding binding = BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with("red")
.noargs();
return binding;
}
/* 让消息成为死信的交换机、队列、绑定等定义 */
// 当向direct.timedExchange交换机发送消息,并且携带red作为路由key,那么此消息会被路由到direct.queue队列
// 并且, 当这个消息设置了过期时间(通过设置MessageProperties#expiration属性), 同时direct.queue又没有消费者,
// 那么, 当到了过期时间时, 这个消息会被发送到该队列所绑定的死信交换机, 并携带原消息原来的路由key,
// 然后, 我们在下面的监听方法中监听死信队列
@Bean
public org.springframework.amqp.core.Exchange directTimedExchange() {
Exchange exchange = ExchangeBuilder.directExchange("direct.timedExchange")
.durable(true)
.build();
return exchange;
}
@Bean
public org.springframework.amqp.core.Queue directQueue() {
org.springframework.amqp.core.Queue queue = QueueBuilder.durable("direct.queue")
// 通过设置参数, 来指定该队列的死信交换机
.withArgument("x-dead-letter-exchange", "dlx.directExchange")
.build();
return queue;
}
@Bean
public Binding exAndQueueBinding() {
// 建立 direct.timedExchange 交换机 到 direct.queue 队列 的绑定关系, 并指定路由key为red
Binding binding = BindingBuilder.bind(directQueue())
.to(directTimedExchange())
.with("red")
.noargs();
return binding;
}
/* 监听死信队列 */
@RabbitListener(queues = {"dlx.queue"})
public void handleDlxMsg(Message message) {
log.info("收到消息=====================");
// 可以在此处观察日志的输出时间, 和消息的数据(我设置的消息的数据就是消息的发送时间)
log.info("message:{},", new String(message.getBody()));
}
}
@Slf4j
@RequestMapping("rabbit")
@RestController
public class RabbitController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("orderMsg")
public Object orderMsg(String expiration, String exchange, String routeKey) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String content = sdf.format(new Date());
rabbitTemplate.convertAndSend(exchange, routeKey, content, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息过期时间
message.getMessageProperties().setExpiration(expiration);
return message;
}
});
return "ok";
}
}
测试步骤:
第一步:发送http://localhost:8081/rabbit/orderMsg?expiration=10000&exchange=direct.timedExchange&routeKey=red,发现确实是在10秒后收到消息
第二步:发送http://localhost:8081/rabbit/orderMsg?expiration=5000&exchange=direct.timedExchange&routeKey=red,发现确实是在5秒后收到消息
第三步:发送完http://localhost:8081/rabbit/orderMsg?expiration=5000&exchange=direct.timedExchange&routeKey=red,接着隔1-2秒发送http://localhost:8081/rabbit/orderMsg?expiration=10000&exchange=direct.timedExchange&routeKey=red,发现1个是在5s后收到消息,1个是在10s后收到消息
第四步:发送完http://localhost:8081/rabbit/orderMsg?expiration=10000&exchange=direct.timedExchange&routeKey=red,接着隔1-2秒发送http://localhost:8081/rabbit/orderMsg?expiration=5000&exchange=direct.timedExchange&routeKey=red,2个消息都是隔10s才收到的消息
这足以证明如果采取这种方案是有问题的,必须是处于消息队列顶端的消息队列到期时,才会立马进入死信队列。所以如果要用这种方案的话,最好是分超时队列,不同的超时时间发送的不同的队列,这样就能保证,最先进入队列的消息先超时,后面的消息也都能正常延迟消费。