生产者端的消息确认
概述
在使用RabbitMQ的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的消息丢失,除此之外我们还会遇到一个问题,当消息的发布者在将消息发送出去之后,消息到底有没有正确到达broker代理服务器呢?如果不进行特殊配置的话,默认情况下发布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达broker的,如果在消息到达broker之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,你怎么进行持久化,那么这个问题该怎么解决呢?
确认机制方案,确保消息能成功发送到对列,并返回给客户端发送结果,如果不能成功发送到队列,就将消息换到一个临时队列中
代码架构
(一)确认交换机
确保消息能够发送到交换机,并返回是否成功
配置文件新增
spring.rabbitmq.publisher-confirm-type=correlated
publisher-confirm-type有3中类型
-
NONE
禁用发布确认模式,是默认值 -
CORRELATED
发布消息成功到交换器后会触发回调方法. -
SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是,waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
代码
配置类
package com.yhd.springbootrabbitmqttl.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 袁浩东
* @description
* @date 2021/7/23 19:56
*/
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//声明业务 Exchange
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
生产者
package com.yhd.springbootrabbitmqttl.confroller;
import com.yhd.springbootrabbitmqttl.MyCallBack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
/**
* @author 袁浩东
* @description
* @date 2021/7/23 20:00
*/
@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message){
//指定消息 id 为 1
CorrelationData correlationData1=new CorrelationData("1");
String routingKey="key1";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correlationData1);
log.info("发送消息内容:{}",message+"key1");
CorrelationData correlationData2=new CorrelationData("2");
routingKey="key2";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey+"2",message+routingKey,correlationData2);
log.info("发送消息内容:{}",message+"key2");
}
}
回调接口
package com.yhd.springbootrabbitmqttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* @author 袁浩东
* @description 回调接口
* @date 2021/7/23 20:02
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
/**
* 交换机不管是否收到消息的一个回调方法
* CorrelationData 保存回调消息的ID及相关信息
* ack 交换机是否收到消息
* cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause)
{
//判断id是否为null
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
}
消费者
package com.yhd.springbootrabbitmqttl.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author 袁浩东
* @description
* @date 2021/7/23 20:03
*/
@Component
@Slf4j
public class ConfirmConsumer {
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
@RabbitListener(queues = CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
log.info("接受到队列 confirm.queue 消息:{}", msg);
}
}
浏览器输入测试地址
http://localhost:8088/confirm/sendMessage/你好
运行结果
总结
消息确认机制,就是利用回调接口去监控生产者端发送的消息是否顺利发送到交换机,返回一个发送结果给客户端。将生产者与回调接口发送消息的id进行绑定
步骤
- yml配置文件新增配置 publisher-confirm-type: correlated
- 配置类声明交换机,队列,并进行绑定
- 声明回调接口
回调接口需要实现RabbitTemplate.ConfirmCallback接口,并重写confirm()方法
confirm()方法的参数说明:
CorrelationData :保存回调消息的ID及相关信息
ack :交换机是否收到消息 ture:收到 false:未收到
cause :失败的原因 - 声明生产者
- 声明消费者
存在的问题
如果消息已经成功发送到交换机,但是没有成功匹配路由key(更改了路由key),消息就不会成功入队列
(二)确认队列
确保消息发送到队列,如果不能成功,则回退消息
实现步骤
yml配置文件新增
publisher-returns: true
回调接口
回调接口实现RabbitTemplate.ReturnsCallback接口,并重写returnedMessage方法
package com.yhd.springbootrabbitmqttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author 袁浩东
* @description 回调接口
* @date 2021/7/23 20:02
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{
@Autowired
RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 交换机不管是否收到消息的一个回调方法
* CorrelationData 保存回调消息的ID及相关信息
* ack 交换机是否收到消息
* cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause)
{
//判断id是否为null
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
//---------------------以下为新增内容------------------------------------
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息被交换机{}退回,原因是{},路由key:{}",returnedMessage,returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
浏览器测试
http://localhost:8088/confirm/sendMessage/你好
测试结果
至此,生产者发出消息到交换机/队列,是否成功,都会返回一个发送的结果。
(三)备份交换机与备份队列
前面的部分是保证了消息可以发送到队列,并返回一个发送结果
什么是备份交换机
备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.7.18/plugins的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警
代码架构图
代码架构图解析
- 一个备份交换机 backup.exchange
- 一个备份队列 backup.queue
- 一个报警队列 warning.queue
代码
配置类新增(config)
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME = "backup.queue";
//报警队列
public static final String WARNING_QUEUE_NAME = "warning.queue";
//声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明报警队列与备份交换机的绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange
backupExchange){
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明备份队列与备份交换机的绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(queue).to(backupExchange);
}
//********************************************
//更改已经声明的确认交换机并与备份交换机进行绑定
//声明确认Exchange
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
// return new DirectExchange(CONFIRM_EXCHANGE_NAME);
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
消费者端报警队列(WarningConsumer)
package com.yhd.springbootrabbitmqttl.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author 袁浩东
* @description 报警队列
* @date 2021/7/24 8:24
*/
@Component
@Slf4j
public class WarningConsumer {
public static final String WARNING_QUEUE_NAME = "warning.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message)
{String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}", msg);
}
}
浏览器输入测试
http://localhost:8088/confirm/sendMessage/你好
测试结果
结果说明
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先
级高,经过上面结果显示答案是备份交换机优先级高
消费者端消息确认
幂等性
概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等
消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给MQ 返回 ack 时网络中断, 故 MQ
未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
解决思路
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ
的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,
这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一
ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现
- 唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id
是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
- Redis 原子性
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
优先级队
使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如
果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用
redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景
优先级队列,就是给消息加上优先级,从而实现优先级高的消息优先被消费
优先级的设置
页面设置
代码设置
队列新增
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
发送消息设置优先级(java版)
AMQP.BasicProperties properties=
new AMQP.BasicProperties().builder().priority(5).build();
优先级队列代码实战(springboot版本)
config配置类
package com.yhd.springbootrabbitmqttl.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author 袁浩东
* @description 优先级队列
* @date 2021/7/24 9:09
*/
@Configuration
public class PriQueueConfig {
private static final String PRI_EXCHANGE="pExchange";
private static final String PRI_QUEUE="pQueue";
//声明交换机
@Bean("priExange")
public DirectExchange priExange(){
return new DirectExchange(PRI_EXCHANGE);
}
//声明队列
@Bean("priQueue")
public Queue priQueue(){
HashMap<String, Object> hashMap = new HashMap<>(3);
//设置队列的优先级为10
hashMap.put("x-max-priority",10);
return QueueBuilder.durable(PRI_QUEUE).withArguments(hashMap).build();
}
//交换机与队列进行绑定
@Bean
public Binding queueBindexange(@Qualifier("priQueue") Queue priQueue,
@Qualifier("priExange") DirectExchange priExange){
return BindingBuilder.bind(priQueue).to(priExange).with("pri");
}
}
消息生产者
package com.yhd.springbootrabbitmqttl.confroller;
import com.yhd.springbootrabbitmqttl.config.PriQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 袁浩东
* @description
* @date 2021/7/24 9:28
*/
@RestController
@RequestMapping("/pri")
@Slf4j
public class PriProducter {
public static final String PRI_EXCHANGE_NAME = "pExchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message){
String routingKey="pri";
rabbitTemplate.convertAndSend(PRI_EXCHANGE_NAME,routingKey,message+routingKey+"999",message1 -> {
message1.getMessageProperties().setPriority(9);
return message1;
});
rabbitTemplate.convertAndSend(PRI_EXCHANGE_NAME,routingKey,message+routingKey+"666",message1 -> {
message1.getMessageProperties().setPriority(6);
return message1;
});
rabbitTemplate.convertAndSend(PRI_EXCHANGE_NAME,routingKey,message+routingKey+"222",message1 -> {
message1.getMessageProperties().setPriority(2);
return message1;
});
}
}
消费者
package com.yhd.springbootrabbitmqttl.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author 袁浩东
* @description
* @date 2021/7/24 9:42
*/
@Component
@RabbitListener(queues ="pQueue")
@Slf4j
public class priConsumer {
@RabbitHandler
public void hand(String str){
log.info("接收到消息{}",str);
}
}
浏览器输入地址测试
http://localhost:8088/pri/sendMessage/你好
运行结果
总结
- 消息的优先级不能超过队列设置的优先级范围
- 队列优先级范围限定在10以内,太大影响性能
- 先设置队列优先级,在设置消息的优先级
队列优先级设置
消息优先级设置
列惰性队列
使用场景
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
如何设置惰性队列
代码设置
//设置队列模式
hashMap.put("x-queue-mode","laze");
return QueueBuilder.durable(PRI_QUEUE).withArguments(hashMap).build();
页面设置
内存开销对比(以发送1百万条消息为例)
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅 占用
1.5MB,惰性队列,消息在磁盘中,内存中存取的是消息的索引 缺点:消费消息速度慢