一、发布确认高级(整合springboot)
前面的发布消息和订阅都建立在MQ可用的情况下。回调的时机两种:一种是MQ服务器收到消息然后给生产者回调;另一种是MQ内部交换机到队列也可能造成消息的丢失,这个时候也会触发回调。
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息(下面的p->b)。只能表名MQ服务器接收到了消息。
如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的(消息的回退)。然而在Broker内部也有消息的发送(e->q)。
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
1.1确认机制方案
1.2模拟交换机应答成功
首先在配置文件中配置,开启交换机应答。不配置这个,写的回调函数将无法被触发
spring.rabbitmq.publisher-confirm-type=correlated
三个参数
- NONE-------禁用发布确认模式,是默认值
- CORRELATED-------发布消息成功到交换器后会触发回调方法
- SIMPLE ----------经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法。 第二种:在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法 等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑。 注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
配置类:
@Configuration
public class ConfirmConfig {
//交换机名
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
//队列名
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//声明交换机
@Bean("confirmE")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
//声明队列
@Bean("confirmQ")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定
@Bean
public Binding queueBinding(@Qualifier("confirmQ")Queue queue,@Qualifier("confirmE") DirectExchange directExchange){
return BindingBuilder.bind(queue).to(directExchange).with("key111");
}
}
生产者:
@Slf4j
@RequestMapping("/confirm")
@RestController
public class Producer {
//交换机名
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
//携带消息内容和消息id
CorrelationData correlationData=new CorrelationData("1");
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key1",message,correlationData);
}
}
消费者:
@Slf4j
@Component
public class ConfirmConsumer {
//队列名
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
@RabbitListener(queues = CONFIRM_QUEUE_NAME)
public void receiveMessage(Message message) throws UnsupportedEncodingException {
String msg = new String(message.getBody(),"UTF-8");
log.info("接收队列是confirm.queue,消费的消息:{}",msg);
}
}
ConfirmCallback回调接口:
@Slf4j
@Component
public class MyconfirmCalback implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回到方法
* @param correlationData ------保存了回到消息的id及相关消息
* @param b------------------交换机是否收到消息 true false
* @param s--------------------失败原因 (成功为null,没原因)
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id= correlationData!=null?correlationData.getId():"";
if(b){
log.info("交换机已经接收到了消息,ID为:{}",id);
}else {
log.info("交换机还未收到id:{}的消息,由于:{}",id,s);
}
}
}
结果分析:
发送成功消息,设置好id,且接收的消息无误,调用回调接口。
发送消息,回调接口打印确认失败的信息给生产者。
当设置路由错误的时候并且仅在设置生产者确认的机制下,交换机接收到消息后,会直接给消息生产者发送确认消息如果。发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的(消息的回退)。
1.3回退消息
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如 果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何 让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
#打开消息回退
spring.rabbitmq.publisher-returns=true
配置类:
@Slf4j
@Component
public class MyconfirmCalback implements RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//实现的是内部类接口,所以要将这个bean作为rabbitTemplate的属性注入
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(this);
}
//通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息:{},被交换机{}退回;退回原因:{},路由:{}",returnedMessage.getMessage().getBody().toString(),
returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
生产者:
@Slf4j
@RequestMapping("/confirm")
@RestController
public class Producer {
//交换机名
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
//携带消息内容和消息id,confirmCallback的参数
CorrelationData correlationData=new CorrelationData("1");
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key1",message,correlationData);
CorrelationData correlationData1=new CorrelationData("2");
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key2",message,correlationData1);
}
}
从下图中看出,id为”1“的消息被交换机接收到并且也被队列接收了。但id为“2”的消息交换机接收了,但是队列将消息回退给了生产者,消息没有被丢失,理由是路由错误。
1.4消息备份
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者 所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。
而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的 复杂性,该怎么做呢?
前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时, 就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由 备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑 定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都 进入这个队列了。
当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
备份结构图:
声明备份交换机和之前声明交换机一样,只是在为一个交换机创建备份交换机的时候,同样要指定,和创建死信队列一样,指定对应的参数。
如图所示,mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。
二、其他知识点
2.1幂等性问题
幂等性问题为解决消息被重复消费。
2.2消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但 实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
2.3解决思路
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消 息时用该 id 先判断该消息是否已消费过。
2.4优先队列
2.4.1使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧。
但是,tmall 商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创 造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存 放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。
添加优先级:
a.队列中代码添加优先级
b.消息中代码添加优先级
c.注意事项
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先 级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序
2.5惰性队列
2.5.1使用场景
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持 更多的消息存储。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留 一份备份。
2.5.2两种模式
队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy 模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过 Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
总结:
1.发布确认高级两次回调,确认回调和消息回退.
2.消息备份,使用备份交换机.
3.优先队列,设置优先级,将消息排序后重新入队被消费者消费.
4.惰性队列,以防消息积压队列造成内存满溢,消息存入磁盘.
5.消息重复消费,幂等性解决.