消息的可靠投递

在使用Rabbitmq的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景.Rabbitmq为我们提供了两种方式用来控制消息的投递可靠性模式

  • confirm确认模式
  • return模式

rabbitmq整个消息投递的路径为:

provider --> rabbitmq broker --> exchange --> queue --> consumer

  • 消息从provider到exchange则会返回一个confireCallback函数
  • 消息从exchange到queue投递失败则会返回一个returnCallback

通过这两个callback控制消息的可靠性投递

confirm确认模式测试

# 在springboot中有三种模式

- NONE值是禁用发布确认模式,是默认值

- CORRELATED值是发布消息成功到交换器后会触发回调方法

- SIMPLE值经测试有两种效果,
其一效果和CORRELATED值一样会触发回调方法,
其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,
要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
# 在配置文件中开启确认模式
spring.rabbitmq.publisher-confirm-type=correlated

测试代码如下:

/**
* 确认模式
* 步骤:
* 1. 在配置文件中开启spring.rabbitmq.publisher-confirm-type=correlated
* 2. 在rabbitTemplate定义ConfirmCallBack函数
*/
@Test
public void testRoute(){ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 相关配置信息
* @param ack exchange交换机是否成功收到了消息 true-成功 false-失败
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm方法被执行了。。");
if(ack){
System.out.println("接收成功消息" + cause);
} else{
System.out.println("接受失败" + cause);
//做一些处理,让消息再次发送
}
}
});
rabbitTemplate.convertAndSend("directs","info","发送info的key的路由消息");
}

结果如下:

return模式测试

# 在配置文件中开启确认模式
spring.rabbitmq.publisher-returns=true

测试代码如下:

/**
* 回退模式: 当消息发送给exchange后,exchange路由到queue失败时才会执行 ReturnCallBack
* 步骤:
* 1. 开启回退模式
* 2. 设置ReturnCallBack
* 3. 设置exchange处理消息的模式:
* 1)如果消息没有路由到Queue,则丢弃消息(默认)
* 2)如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn(){ //设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true); //设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return执行了....");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey); //错误之后的逻辑处理
}
}); rabbitTemplate.convertAndSend("directs","","发送info的key的路由消息");
}

我们这里故意把routingKey设为了空,人为的制造了错误

测试结果如下:

总结:

  • 设置publisher-confirm-type=correlated 开启确认模式
  • 使用rabbitTemplate.setConfirmCallback设置回调函数.当消息发送到exchange后回调confirm方法.在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理

Consumer Ack

ack指Acknowledge,确认.表示消费端收到消息后的确认方式

有三种确认方式:

  • 自动确认: acknowledge="none"
  • 手动确认: acknowledge="manual"
  • 根据异常情况确认: acknowledge="auto"(不建议使用)

自动确认是指: 当消息一旦被Consumer接收到,则自动确认,并将相应的message从RabbitMQ的消息缓存中移除.但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失.

如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel,basicNack()方法,让其自动重新发送消息

测试代码如下:

/**
* 1. 设置手动签收 ackMode="manual"
* 2. 如果消息成功处理,则调用channel的basicAck()方法
* 3. 如果消息处理失败,则调用channel的basicNack()拒绝签收,然后broker重新发送给consumer
* @param message
*/
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //创建临时队列
exchange = @Exchange(value = "directs",type = "direct"), //绑定交换机
key = {"info"} //指定路由的key
)
},ackMode = "MANUAL") //设置手动签收模式
public void receiveByAck(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println("message = " + new String(message.getBody())); System.out.println("处理业务逻辑");
int i = 3/0; //模拟业务逻辑出现错误 /**
* 参数1: 传递过来的消息的标签
* 参数2: 是否接受多条消息
*/
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
/**
* 拒绝签收
* 参数3: 重回队列,如果设置为true,则消息重新回到queue,broker会重新发送给客户端
*/
channel.basicNack(deliveryTag,true,true);
}
}

当出现错误后的结果:

当业务逻辑恢复正常后的结果

消息可靠性的四个方面

  1. 消息持久化

    • exchange要持久化
    • queue要持久化
    • message要持久化
  2. 生产方确认 Confirm
  3. 消息方确认 Ack
  4. Broker高可用 镜像集群搭建

消费端限流

在配置文件中配置

spring.rabbitmq.listener.simple.prefetch=15

测试代码

/**
* @PROJECT_NAME: myTest
* @DESCRIPTION: 消费者
* 1. 确保ack机制为手动确认
* 2. 在配置文件中配置 spring.rabbitmq.listener.direct.prefetch=1
* - 表示消费端每次从mq拉取一条消息来消费,直到手动确认消费完毕后,才会继续拉取下一条
* @USER: 罗龙达
* @DATE: 2021/2/17 1:54
*/
@Component
public class HelloConsumer { @RabbitListener(queuesToDeclare = @Queue(value = "hello"),ackMode = "MANUAL")
public void receive(Message message, Channel channel) throws IOException, InterruptedException { System.out.println("message = " + new String(message.getBody())); System.out.println("处理业务逻辑..");
Thread.sleep(1000); channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); //手动确认消息 }
}

这样consumer就会15条15条的拉取消息


TTL

  • TTL全称 Time To Live(存活时间)
  • 当消息到达存活时间后,还没有被消费,会被自动清除
  • RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间

设置过期队列测试代码如下:

@RabbitListener(queuesToDeclare = @Queue(value = "test_queue_ttl",
arguments = @Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Long"))) //设置队列的ttl属性 type记得改成long类型
public void receiveTTl(Message message, Channel channel) throws IOException, InterruptedException { System.out.println("message = " + new String(message.getBody())); System.out.println("处理业务逻辑..");
Thread.sleep(1000); channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); }

消息单独过期测试代码如下:

@Test
public void testTopic() {
rabbitTemplate.convertAndSend("topics", "delete.order", "基于delete.order的路由消息",new MessagePostProcessor() {
/**
*设置消息单独过期的方法
* 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准.
* 队列过期后,会将所有的消息全部移除
* 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)
*/
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");
return message;
}
});
}

死信队列

死信队列:英文缩写:DLX.

Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到拎一个交换机,这个交换机就是DLX

消息成为死信的三种情况:

  1. 队列消息长度达到限制
  2. 消费者拒绝消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  3. 原队列存在消息过期设置,消息到达超时时间未被消费

队列绑定死信交换机:

​ 给队列设置参数:x-dead-letter-exchangex-dead-letter-routing-key

测试代码如下:

    @RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "test_DLX",
arguments = { //这里的argument一定不要加错了地方!!
@Argument(name = "x-dead-letter-exchange",value = "DLX"), //指定死信交换机
@Argument(name = "x-dead-letter-routing-key",value = "deadKey"), //指定死信交换机的routingkey
@Argument(name = "x-message-ttl",value = "5000",type = "java.lang.Long"), //指定消息过期时间
@Argument(name = "x-max-length",value = "5",type = "java.lang.Long") //指定最大长度,当发送的消息超过了这个数就会进入死信队列), //创建临时队列
}
),
exchange = @Exchange(value = "directs",type = "direct"), //绑定交换机
key = {"info","warning","error"} //指定路由的key )
})
public void receiveDLX(String message){
System.out.println("message1 = " + message);
}

总结:

  1. 死信交换机和死信队列和普通的交换机队列没啥区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为私心的三种情况
    1. 队列消息长度达到限制
    2. 消费者拒绝消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
    3. 原队列存在消息过期设置,消息到达超时时间未被消费

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费

需求:

1. 下单后,30分钟未支付,取消订单,回滚库存
2. 新用户注册成功7天后,发送短信问候

实现方式:

1. 定时器(创建订单的时候同时上传创建时间,写一段代码以轮询的方式去访问库表,当前时间与创建时间差值在30分钟以上的就删除订单)
2. 延迟队列

但是在RabbitMQ中并未提供延迟队列功能...

但是可以使用:TTL + 死信队列组合实现延迟队列的效果

测试代码如下:

    @RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "test_Delay",arguments = {
@Argument(name = "x-dead-letter-exchange",value = "DeadLetterEx"), //指定死信交换机
@Argument(name = "x-dead-letter-routing-key",value = "cancel"), //指定死信交换机的routingKey
@Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Long"), //超时时间为10s
@Argument(name = "x-max-length",value = "3",type = "java.lang.Long") //队列最大长度为3
}), //创建普通队列
exchange = @Exchange(value = "test_delay_exchange",type = "direct"), //绑定交换机
key = {"info","warning","error"} //指定路由的key
)
},ackMode = "MANUAL")
public void receiveDelay(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println("message = " + new String(message.getBody())); System.out.println("处理业务逻辑");
int i = 3/0; //模拟业务逻辑出现错误 /**
* 参数1: 传递过来的消息的标签
* 参数2: 是否接受多条消息
*/
channel.basicAck(deliveryTag,true);
System.out.println("业务逻辑处理完毕");
} catch (Exception e) {
/**
* 拒绝签收
* 参数3: 重回队列,如果设置为true,则消息重新回到queue,broker会重新发送给客户端
*/
System.out.println("执行拒绝签收");
channel.basicNack(deliveryTag,true,false);
}
} @RabbitListener(bindings = {
@QueueBinding(
value = @Queue("receiveOrder"), //创建一个队列,监听死信交换机
exchange = @Exchange(value = "DeadLetterEx",type = "direct"),
key = {"cancel"})
})
public void receiveOrder(String message){
System.out.println("判断订单状态 " + message + new Date());
}

消息可靠性保障

需求:如何保证消息100%传递成功?

一开始的业务逻辑可能是这样的

生产者发送消息到消息队列,消费者消费。但是如果producer的业务数据入库了,但是发送消息失败了,consumer就接受不到消息了。因此我们的架构需要改进

如果说前面那条消息consumer接收成功了,就发送确认消息到Q2队列里,并调用回调服务写到另一个检测数据库里,

当producer发完消息后的一段时间里再向Q3里发送一条一模一样的消息,并通过回调服务拿到检测数据库里去比对,比对这条消息跟刚才发送的那条消息的id,

如果有这条消息的id代表consumer正常接收消息,就什么都不做,如果找不到匹配的消息,就让producer重新发一次消息

假设极端情况,发送消息失败了,发送延迟消息也失败了,这个时候架构又得改进

添加一个定时检查服务,每个几个小时就看一看MDB和DB里的消息id是否一致,不匹配的就让producer重新发送消息

消息幂等性保障

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说。其任意多次执行对资源本身所产生的影响均与依次执行的影响相同。

​ 在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果

乐观锁机制

比如说consumer宕机了几分钟Q1队列里堆积了两条或者多条消息,如果不做幂等性保障,可能会导致多次扣款等情况发生,因此我们加入version版本号

第一次执行:version=1

update account set money = money -500, version = version + 1 ,where id = 1 and version = 1;

第二次执行:version=2

update account set money = money -500, version = version + 1 ,where id = 1 and version = 1;

这个时候数据库中没有匹配的记录,update就不会执行,这样就保障了一条消息不会重复执行


RabbitMQ高级特性的更多相关文章

  1. RabbitMQ(二):RabbitMQ高级特性

    RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用.作为一名合格的开发者,有必要了解一下相关知识,RabbitMQ(一)已经入门RabbitMQ,本文介绍Rabb ...

  2. RabbitMQ实战(三)-高级特性

    0 相关源码 1 你将学到 如何保证消息百分百投递成功 幂等性 如何避免海量订单生成时消息的重复消费 Confirm确认消息.Return返回消息 自定义消费者 消息的ACK与重回队列 限流 TTL ...

  3. RabbitMQ的基本使用到高级特性

    简介 继上一篇 CentOS上安装RabbitMQ讲述RabbitMQ具体安装后,这一篇讲述RabbitMQ在C#的使用,这里将从基本用法到高级特性的使用讲述. 前序条件 这里需要增加一个用户,并且设 ...

  4. 消息中间件——RabbitMQ(七)高级特性全在这里!(上)

    前言 前面我们介绍了RabbitMQ的安装.各大消息中间件的对比.AMQP核心概念.管控台的使用.快速入门RabbitMQ.本章将介绍RabbitMQ的高级特性.分两篇(上/下)进行介绍. 消息如何保 ...

  5. 消息中间件——RabbitMQ(八)高级特性全在这里!(下)

    前言 上一篇消息中间件--RabbitMQ(七)高级特性全在这里!(上)中我们介绍了消息如何保障100%的投递成功?,幂等性概念详解,在海量订单产生的业务高峰期,如何避免消息的重复消费的问题?,Con ...

  6. Rabbitmq之高级特性——百分百投递消息&消息确认模式&消息返回模式实现

    rabbitmq的高级特性: 如何保障消息的百分之百成功? 要满足4个条件:生产方发送出去,消费方接受到消息,发送方接收到消费者的确认信息,完善的消费补偿机制 解决方案,1)消息落库,进行消息状态打标 ...

  7. 消息队列——RabbitMQ的基本使用及高级特性

    文章目录 一.引言 二.基本使用 1. 简单示例 2. work queue和公平消费消息 3. 交换机 三.高级特性 1. 消息过期 2. 死信队列 3. 延迟队列 4. 优先级队列 5. 流量控制 ...

  8. ActiveMQ中的Destination高级特性(一)

    ---------------------------------------------------------------------------------------- Destination ...

  9. Python3学习(二)-递归函数、高级特性、切片

    ##import sys ##sys.setrecursionlimit(1000) ###关键字参数(**关键字参数名) ###与可变参数不同的是,关键字参数可以在调用函数时,传入带有参数名的参数, ...

随机推荐

  1. react项目运行安装依赖报错:Error: pngquant failed to build, make sure that libpng-dev is installed

    安装报错之后.但是安装libpng-dev.发现找不到.通过多方查找.准备重新安装pngquant.命令如下: npm install --save-dev pngquant安装成功并运行成功

  2. 快速了解 JavaScript ES2019 的五个新增特性

    ES2019 规范是对 JavaScript 的一个较小的补充,但它仍然带来了一些有用的功能.本文将向你展示五个 ES2019 新增的特性,这些特性或许可以让你的编程轻松一点.这些特性包括 trimS ...

  3. C# WebView2 在你的应用中使用Chromium内核

    什么是WebView2? Win10上对标Edge浏览器 Chromium内核 简单的可视为WebBrowser组件的升级版 如何使用WebView2? 官网下载 WebView2 RunTime V ...

  4. Java(133-151)【String类、static、Arrays类、Math类】

    1.字符串概述和特点 string在lang包里面,因此可以直接使用 字符串的内容不可变 2.字符串的构造方法和直接创建 三种构造方法 package cn.itcast.day08.demo01; ...

  5. 自动化kolla-ansible部署ubuntu20.04+openstack-victoria之系统安装-03

    自动化kolla-ansible部署ubuntu20.04+openstack-victoria之系统安装-03  欢迎加QQ群:1026880196  进行交流学习 一.镜像下载 网易源: http ...

  6. Java 8 Optional

    这是一个可以为null的容器对象.如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象. package com.polaris; import java.util.A ...

  7. 在IntellJ中查看JavaDoc

    1. [perference--Editor--General--Code Completion] 勾上Show the documentation popup in ** ms  2. 快速显示Ja ...

  8. Salesforce学习之路(五)role hierarchy & sharing

    1. Role Hierarchy 在私有或者混合模型中,如果在organization-wide defaults设置某个对象为Private,那么对象的记录只有拥有者可以查看.但是,role hi ...

  9. day12.函数其它与模块1

    一.函数递归 函数的递归调用:是函数嵌套调用的一种特殊形式 具体指的是在调用一个函数的过程中又直接或者间接地调用自己,称之为函数的递归调用 函数的递归调用其实就是用函数实现的循环 # def f1() ...

  10. vagrant构建centos虚拟环境

    vagrant搭建centos 什么是vagrant 如何使用 1.构建本地的目录 2.官方下载对应的镜像文件,官方下载地址 3.导入刚刚下载的镜像(box文件) 4.初始化 5.修改Vagrantf ...