当前位置:

首页 > 编程开发 > Java RabbitMQ如何实现持久化和发布确认

Java RabbitMQ如何实现持久化和发布确认

1.持久化当RabbitMQ服务停掉以后消息生产者发送过的消息不丢失。默认情况下RabbitMQ退出或者崩溃时,会忽视掉队列和消息。为了保证消息不丢失需要将队列和消息都标记为持久化。1.1实现持久化1.队列持久化:在创建队列时将channel.queueDeclare();第二个参数改为true。2.消息持久化:在使用信道发送消息时channel.basicPublish();将第三个参数改为:MessageProperties.PERSISTENT_TEXT_PLAIN表示持久化消息。/***@Desc

本文目录

      1. 持久化

      当RabbitMQ服务停掉以后消息生产者发送过的消息不丢失。默认情况下RabbitMQ退出或者崩溃时,会忽视掉队列和消息。为了保证消息不丢失需要将队列和消息都标记为持久化。

      1.1 实现持久化

      1.队列持久化:在创建队列时将channel.queueDeclare();第二个参数改为true。

      2.消息持久化:在使用信道发送消息时channel.basicPublish();将第三个参数改为:MessageProperties.PERSISTENT_TEXT_PLAIN表示持久化消息。

      /**
       * @Description 持久化MQ
       * @date 2022/3/7 9:14
       */
      public class Producer3 {
          private static final String LONG_QUEUE = "long_queue";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              // 持久化队列
              channel.queueDeclare(LONG_QUEUE,true,false,false,null);
              Scanner scanner = new Scanner(System.in);
              int i = 0;
              while (scanner.hasNext()){
                  i++;
                  String msg = scanner.next() + i;
                  // 持久化消息
                  channel.basicPublish("",LONG_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes(StandardCharsets.UTF_8));
                  System.out.println("发送消息:'" + msg + "'成功");
              }
          }
      }

      但是存储消息还有存在一个缓存的间隔点,没有真正的写入磁盘,持久性保证不够强,但是对于简单队列而言也绰绰有余。

      1.2 不公平分发

      轮询分发的方式在消费者处理效率不同的情况下并不适用。所以真正的公平应该是遵循能者多劳的前提。

      在消费者处修改channel.basicQos(1);表示开启不公平分发

      /**
       * @Description 不公平分发消费者
       * @date 2022/3/7 9:27
       */
      public class Consumer2 {
          private static final String LONG_QUEUE = "long_queue";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              DeliverCallback deliverCallback = (consumerTag, message) -> {
                  // 模拟并发沉睡三十秒
                  try {
                      Thread.sleep(30000);
                      System.out.println("线程B接收消息:"+ new String(message.getBody(), StandardCharsets.UTF_8));
                      channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              };
              // 设置不公平分发
              channel.basicQos(1);
              channel.basicConsume(LONG_QUEUE,false,deliverCallback,
                      consumerTag -> {
                          System.out.println(consumerTag + "消费者取消消费");
                      });
          }
      }

      1.3 测试不公平分发

      测试目的:是否能实现能者多劳。

      测试方法:两个消费者睡眠不同的事件来模拟处理事件不同,如果处理时间(睡眠时间)短的能够处理多个消息就代表目的达成。

      先启动生产者创建队列,再分别启动两个消费者。

      生产者按照顺序发四条消息:

      Java RabbitMQ如何实现持久化和发布确认

      睡眠时间短的线程A接收到了三条消息

      Java RabbitMQ如何实现持久化和发布确认

      而睡眠时间长的线程B只接收到的第二条消息:

      Java RabbitMQ如何实现持久化和发布确认

      因为线程B在处理消息时消耗的时间较长,所以就将其他消息分配给了线程A。

      实验成功!

      1.4 预取值

      消息的发送和手动确认都是异步完成的,因此就存在一个未确认消息的缓冲区,开发人员希望能够限制缓冲区的大小,用来避免缓冲区里面无限制的未确认消息问题。

      这里的预期值就值得是上述方法channel.basicQos();里面的参数,如果在当前信道上存在等于参数的消息就不会在安排当前信道进行消费消息。

      1.4.1 代码测试

      测试方法:

      1.新建两个不同的消费者分别给定预期值5个2。

      2.给睡眠时间长的指定为5,时间短的指定为2。

      3.假如按照指定的预期值获取消息则表示测试成功,但并不是代表一定会按照5和2分配,这个类似于权重的判别。

      代码根据上述代码修改预期值即可。

      2. 发布确认

      发布确认就是生产者发布消息到队列之后,队列确认进行持久化完毕再通知给生产者的过程。这样才能保证消息不会丢失。

      需要注意的是需要开启队列持久化才能使用确认发布。
      开启方法:channel.confirmSelect();

      2.1 单个确认发布

      是一种同步发布的方式,即发送完一个消息之后只有确认它确认发布后,后续的消息才会继续发布,在指定的时间内没有确认就会抛出异常。缺点就是特别慢。

      /**
       * @Description 确认发布——单个确认
       * @date 2022/3/7 14:49
       */
      public class SoloProducer {
          private static final int MESSAGE_COUNT = 100;
          private static final String QUEUE_NAME = "confirm_solo";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              // 产生队列
              channel.queueDeclare(QUEUE_NAME,true,false,false,null);
              // 开启确认发布
              channel.confirmSelect();
              // 记录开始时间
              long beginTime = System.currentTimeMillis();
              for (int i = 0; i < MESSAGE_COUNT; i++) {
                  String msg = ""+i;
                  channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes(StandardCharsets.UTF_8));
                  // 单个发布确认
                  boolean flag = channel.waitForConfirms();
                  if (flag){
                      System.out.println("发送消息:" + i);
                  }
              }
              // 记录结束时间
              long endTime = System.currentTimeMillis();
              System.out.println("发送" + MESSAGE_COUNT + "条消息消耗:"+(endTime - beginTime) + "毫秒");   }
      }

      2.2 批量确认发布

      一批一批的确认发布可以提高系统的吞吐量。但是缺点是发生故障导致发布出现问题时,需要将整个批处理保存在内存中,后面再重新发布。

      /**
       * @Description 确认发布——批量确认
       * @date 2022/3/7 14:49
       */
      public class BatchProducer {
          private static final int MESSAGE_COUNT = 100;
          private static final String QUEUE_NAME = "confirm_batch";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              // 产生队列
              channel.queueDeclare(QUEUE_NAME,true,false,false,null);
              // 开启确认发布
              channel.confirmSelect();
              // 设置一个多少一批确认一次。
              int batchSize = MESSAGE_COUNT / 10;
              // 记录开始时间
              long beginTime = System.currentTimeMillis();
              for (int i = 0; i < MESSAGE_COUNT; i++) {
                  String msg = ""+i;
                  channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes(StandardCharsets.UTF_8));
                  // 批量发布确认
                  if (i % batchSize == 0){
                      if (channel.waitForConfirms()){
                          System.out.println("发送消息:" + i);
                      }
                  }
              }
              // 记录结束时间
              long endTime = System.currentTimeMillis();
              System.out.println("发送" + MESSAGE_COUNT + "条消息消耗:"+(endTime - beginTime) + "毫秒");
          }
      }

      显然效率要比单个确认发布的高很多。

      2.3 异步确认发布

      在编程上比上述两个要复杂,但是性价比很高,无论是可靠性还行效率的都好很多,利用回调函数来达到消息可靠性传递的。

      /**
       * @Description 确认发布——异步确认
       * @date 2022/3/7 14:49
       */
      public class AsyncProducer {
          private static final int MESSAGE_COUNT = 100;
          private static final String QUEUE_NAME = "confirm_async";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              // 产生队列
              channel.queueDeclare(QUEUE_NAME,true,false,false,null);
              // 开启确认发布
              channel.confirmSelect();
              // 记录开始时间
              long beginTime = System.currentTimeMillis();
              // 确认成功回调
              ConfirmCallback ackCallback = (deliveryTab,multiple) ->{
                  System.out.println("确认成功消息:" + deliveryTab);
              };
              // 确认失败回调
              ConfirmCallback nackCallback = (deliveryTab,multiple) ->{
                  System.out.println("未确认的消息:" + deliveryTab);
              };
              // 消息监听器
              /**
               * addConfirmListener:
               *                  1. 确认成功的消息;
               *                  2. 确认失败的消息。
               */
              channel.addConfirmListener(ackCallback,nackCallback);
              for (int i = 0; i < MESSAGE_COUNT; i++) {
                  String msg = "" + i;
                  channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes(StandardCharsets.UTF_8));
              }
      
              // 记录结束时间
              long endTime = System.currentTimeMillis();
              System.out.println("发送" + MESSAGE_COUNT + "条消息消耗:"+(endTime - beginTime) + "毫秒");
          }
      }

      2.4 处理未确认的消息

      最好的处理方式把未确认的消息放到一个基于内存的能被发布线程访问的队列。

      例如:ConcurrentLinkedQueue可以在确认队列confirm callbacks与发布线程之间进行消息的传递。

      处理方式:

      1.记录要发送的全部消息;

      2.在发布成功确认处删除;

      3.打印未确认的消息。

      使用一个哈希表存储消息,它的优点:

      可以将需要和消息进行关联;轻松批量删除条目;支持高并发。

      ConcurrentSkipListMap map = new ConcurrentSkipListMap<>();
      /**
       * @Description 异步发布确认,处理未发布成功的消息
       * @date 2022/3/7 18:09
       */
      public class AsyncProducerRemember {
          private static final int MESSAGE_COUNT = 100;
          private static final String QUEUE_NAME = "confirm_async_remember";
          public static void main(String[] args) throws Exception {
              Channel channel = RabbitMQUtils.getChannel();
              // 产生队列
              channel.queueDeclare(QUEUE_NAME,true,false,false,null);
              // 开启确认发布
              channel.confirmSelect();
              // 线程安全有序的一个hash表,适用与高并发
              ConcurrentSkipListMap< Long, String > map = new ConcurrentSkipListMap<>();
              // 记录开始时间
              long beginTime = System.currentTimeMillis();
              // 确认成功回调
              ConfirmCallback ackCallback = (deliveryTab, multiple) ->{
                  //2. 在发布成功确认处删除;
                  // 批量删除
                  if (multiple){
                      ConcurrentNavigableMap confirmMap = map.headMap(deliveryTab);
                      confirmMap.clear();
                  }else {
                      // 单独删除
                      map.remove(deliveryTab);
                  }
                  System.out.println("确认成功消息:" + deliveryTab);
              };
              // 确认失败回调
              ConfirmCallback nackCallback = (deliveryTab,multiple) ->{
                  // 3. 打印未确认的消息。
                  System.out.println("未确认的消息:" + map.get(deliveryTab) + ",标记:" + deliveryTab);
              };
              // 消息监听器
              /**
               * addConfirmListener:
               *                  1. 确认成功的消息;
               *                  2. 确认失败的消息。
               */
              channel.addConfirmListener(ackCallback,nackCallback);
              for (int i = 0; i < MESSAGE_COUNT; i++) {
                  String msg = "" + i;
                  channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes(StandardCharsets.UTF_8));
                  // 1. 记录要发送的全部消息;
                  map.put(channel.getNextPublishSeqNo(),msg);
              }
      
              // 记录结束时间
              long endTime = System.currentTimeMillis();
              System.out.println("发送" + MESSAGE_COUNT + "条消息消耗:"+(endTime - beginTime) + "毫秒");
          }
      }
      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。