消息的可靠投递

在使用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. python3 int() 各数据类型转int

    print(int('0b1010',0))#二进制数print(int('0xa',0))#十六进制数print(int('0xa',16))print(int('a',16))print(int( ...

  2. C语言函数调用完整过程

    C语言函数调用详细过程 函数调用是步骤如下: 按照调用约定传参 调用约定是调用方(Caller)和被调方(Callee)之间按相关标准 对函数的某些行为做出是商议,其中包括下面内容: 传参顺序:是从左 ...

  3. VSCode中插件Code Spell Checker

    说在前面 介绍 Code Spell Checker 是在VSCode中的一款插件,能够帮助我们检查单词拼写是否出现错误,检查的规则遵循 camelCase (驼峰拼写法). 安装方法 打开VSCod ...

  4. PAT (Basic Level) Practice (中文)1078 字符串压缩与解压 (20 分) 凌宸1642

    PAT (Basic Level) Practice (中文)1078 字符串压缩与解压 (20 分) 凌宸1642 题目描述: 文本压缩有很多种方法,这里我们只考虑最简单的一种:把由相同字符组成的一 ...

  5. Istio 网络弹性 实践 之 故障注入 和 调用重试

    网络弹性介绍 网络弹性也称为运维弹性,是指网络在遇到灾难事件时快速恢复和继续运行的能力.灾难事件的范畴很广泛,比如长时间停电.网络设备故障.恶意入侵等. 重试(attempts) Istio 重试机制 ...

  6. Chrome最新0day RCE(2021/4/13)

    关于Chrome Chrome就是Google浏览器... POC Git链接 https://github.com/r4j0x00/exploits/tree/master/chrome-0day ...

  7. day-5 xctf-when_did_you_born

    xctf-when_did_you_born 题目传送门:https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade ...

  8. baystack(ret2one_gadget)

    babystack 首先检查一下保护 全保护开启,我们IDA分析一下. main函数很简单,首先第一个read明显存在漏洞,如果不是以 \n 结尾会存在栈中地址的泄漏. payload = 'A'*0 ...

  9. Linux入门之基本的概念、安装和操作

    目录 Linux基本概念 Linux的安装 虚拟机安装CentOS7 CentOS设置网络 Linux基本操作命令 文件目录操作命令 进程操作命令 文本操作命令 Linux权限操作 用户和组操作命令 ...

  10. Day14_76_反射与静态语句块

    反射与静态语句块 * 获取class对象与静态语句块的关系 package com.shige.Reflect; import java.nio.channels.ClosedSelectorExce ...