消息的可靠投递

在使用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. golang 性能调优分析工具 pprof(下)

    golang 性能调优分析工具 pprof(上)篇, 这是下篇. 四.net/http/pprof 4.1 代码例子 1 go version go1.13.9 把上面的程序例子稍微改动下,命名为 d ...

  2. python基础(一):变量和常量

    变量 什么是变量 变量,用于在内存中存放程序数据的容器.计算机的核心功能就是"计算",CPU是负责计算的,而计算需要数据吧?数据就存放在内存里,例如:将梁同学的姓名,年龄存下来,让 ...

  3. 201871030118-雷云云 实验二 个人项目—D{0-1}背包问题项目报告

    项目 内容 课程班级博客链接 班级博客 这个作业要求链接 作业链接 我的课程学习目标 1.了解并掌握psp2.掌握软件项目个人开发流程3.掌握Github发布软件项目的操作方法 这个作业在哪些方面帮助 ...

  4. BUAA_2021_SE_READING_#2

    项目 内容 这个作业属于哪个课程 2021春季软件工程(罗杰 任健) 这个作业的要求在哪里 个人阅读作业#2 我在这个课程的目标是 通过课程学习,完成第一个可以称之为"软件"的项目 ...

  5. spieces-in-pieces动画编辑器

    前言: 制作灵感来源于 http://species-in-pieces.com/ 这个网站,此网站作者是来自阿姆斯特丹的设计师 Bryan James,其借用纯CSS技术表现出30种濒危动物的碎片拼 ...

  6. Spring (二)SpringIoC和DI注解开发

    1.Spring配置数据源 1.1 数据源(连接池)的作用 数据源(连接池)是提高程序性能出现的 事先实例化数据源,初始化部分连接资源 使用连接资源时从数据源中获取 使用完毕后将连接资源归还给数据源 ...

  7. redhat7.6 安装java和tomcat

    使用yum 安装java # 首先查看是否安装yum rpm -qa | grep yum yum-3.4.3-161.el7.noarch # 显示这个表示已经安装了. # 查看是否安装java,没 ...

  8. Redis——急速安装并设置自启(CentOS)

    现状 对于开发人员来说,部署服务器环境并不是一个高频操作.所以就导致绝大部分开发人员不会花太多时间去学习记忆,而是直接百度(有一些同学可能连链接都懒得收藏).所以到了部署环境的时候就头疼,甚至是抗拒. ...

  9. C#入门到精通系列课程——第1章软件开发及C#简介

    ◆本章内容 (1)了解软件 (2)软件开发相关概念 (3)认识.NET Framework (4)C#语言 (5)Visual Studio 2017 ◆本章简述 软件在现代人们的日常生活中随处可见, ...

  10. Alpine镜像

    Alpine Linux 是一个面向安全,轻量级的基于musl libc与busybox项目的Linux发行版. Alpine 提供了自己的包管理工具 apk,可以通过 https://pkgs.al ...