可靠性:
消息队列的可靠性是指消息队列系统保证消息能够准确、完整且及时地从发送方传递到接收方的能力。
要实现消息的可靠性主要在消息传递的三个过程:生产者-消息队列,消息队列持久化,消息队列-消费者。
生产者-消息队列
理论:
解决这一过程中可能出现的消息丢失主要是生产者确认机制。主要的流程是生产者发送消息给消息队列,消息队列如果成功收到消息会返回一个ACK告知生产者消息投递成功,如果出现错误会返回NACK告知消息投递失败。
ps:消息队列收到消息后会返回的结果有以下几种情况:
实践(rabbitMQ的java实现):
在yml文件中配置:
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1
# 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
在rabbitTemplate中设置一个回调机制
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置returnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
}
}
生产者确认
void testPublisherConfirm() throws InterruptedException {
// 1.创建CorrelationData
CorrelationData cd = new CorrelationData();
// 2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("handle message ack fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到确认的处理逻辑,参数中的result就是确认内容
if(result.isAck()){
// 发送消息成功,收到ack
log.debug("发送消息成功,收到ack");
}else{
// 发送消息失败,收到nack
log.error("发送消息失败,收到nack,reason+:{}", result.getReason());
//重发消息(这里需要根据业务来制定重发消息的策略)
}
}
});
// 3.发送消息
rabbitTemplate.convertAndSend("hmall.direct", "red1", "hello", cd);
}
ps:经过测试,开启了生产者重试会有一定的性损耗
消息队列持久化
消息队列持久化有两种方式:
1.设置交换机和队列持久化,然后再设置消息持久化
设置交换机和队列持久化在java中实现只需要将各自的durable=true即可,设置消息持久化只需要在发送消息的消息体做如下设置:
Message message = MessageBuilder
.withBody("hello".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
2.使用lazy Queue,在RabbitMQ3.12版本后所有队列默认是lazy Queue,这种队列会将收到的消息直接存储在磁盘中,当消费者需要消息时才会加载到内存,非3.12以上版本可以通过以下注解设置:
@RabbitListener(queuesToDeclare = @Queue( name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy") ))
消息队列-消费者
理论:
消费者确认机制:当消费收到消息并处理成功后会向消息队列发送一个回执,回执一般有三种可选值:
失败重试机制 :
以上在yml文件中的配置如下:
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: auto # 确认机制
retry:
enabled: true
initial-interval: 1000ms #重试间隔
multiplier: 1 #下次失败等待时间的倍数
max-attempts: 3 #最大重试次数
消息失败处理策略:
在实际开发中常用的是最后一种处理策略
第三种策略代码实现如下:
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry", name = "enable", havingValue = "true")
public class ErrorConfiguration {
@Bean
public DirectExchange errorExchange(){
return new DirectExchange(name: "error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue(name: "error.queue");
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with(routingKey: "error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,
errorExchange: "error.direct",
errorRoutingKey: "error");
}
}
幂等性:
消息的幂等性指的是,在分布式系统中执行一次消息产生的结果和执行多次消息产生的结果一致,比如用户下单后会向消息队列发送一条消息,短信系统收到消息后会发送短信给用户,如果这个时候消息队列所在的服务器重启了,当消息队列重启后又会向短信系统发送一条重复消息,该短信系统需要能够识别出这是一条重复消息。
具体实现方式:
最通用的方案就是为一条消息生成一个全局id并维护一个消息id和消息的数据表,为消息生成全局id只需要在生产者的启动类中添加以下的bean即可。
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class, args);
}
@Bean
public MessageConverter jacksonMessageConvertor() {
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
jjmc.setCreateMessageIds(true); // 创建消息ID
return jjmc;
}
}
延时消息:
当消息被发送到消息队列之后,并不会立即被消费者接收和处理,而是要等到预定的时间点或经过了一定的时间间隔之后才会变为可消费的状态。
延时消息的主要应用场景包括但不限于:
- 订单超时取消与退款
- 活动触发(如生日祝福、节假日促销等)
- 自动任务执行(例如定期备份、数据同步等)
- 事务状态确认(如淘宝的七天无理由退货政策下的自动确认收货)
实现延时消息有两种方法:
死信交换机:
延迟消息插件:
启用插件需要在docker中运行以下命令:
docker exec -it 容器名 #进入容器
rabbitmq-plugins enable rabbitmq_delayed_message_exchange #启动插件
发送消息的实现代码如下:
@Test
void testPublisherDelayMessage() {
// 1. 创建消息
String message = "hello, delayed message";
// 2. 发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
取消超时订单设计思路:
具体的思路应该是将30分钟的消息分成多个消息上传,比如将30分钟的消息分成9,8,6,7分钟四个消息每次发送一条消息。