RabbitMQ 学习(二) —— 可靠性投递

生产端可靠性投递

保证生产端可靠性投递需要以下几步:
1、保障消息端成功发出。
2、保障MQ节点的成功接收。
3、发送端收到MQ节点(Broker)确认应答。
4、完善端消息进行补偿机制。

方案一:

1、对业务数据和消息数据进行落库。
2、发送消息并监听回调,如果收到ACK,更数据库消息状态。
3、分布式定时任务查找消息状态不是成功的消息进行再次投递。

在这里插入图片描述
总结:这种方式需要多次访问数据库,在高并发的场景可能不是很合适。

方案二:
1、对业务数据进行存储,然后发送MQ消息。并再发送一个延迟消息
2、消费者收到消息后,发送一个新的消息。
3、需要一个Callback 服务处理消费者发送的消息,将消息入库。同时接收生产者发送的延迟消息。当延迟的消息在库中存在则代表消息成功消费,否则,Callback会向生产者发送RPC通信,生产者重新找到业务数据,重新发送。
在这里插入图片描述

Confirm 确认消息

Confirm消息确认机制:

  • 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
  • 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息到可靠性投递到核心保障

生产者发送消息后,可以创建一个监听等待Broker的回调。
在这里插入图片描述

如何实现Confirm确认消息

第一步:在channel上开启确认模式:channel.confirmSelect()
第二步:在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、记录日志等后续处理!

代码实现:

生产者:

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个connectionFactory,并进行配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");

        //通过连接工厂 创建连接
        Connection connection = connectionFactory.newConnection();

        //通过 connection 创建一个 channel
        Channel channel = connection.createChannel();

        //指定我们的消息投递模式:消息的确认模式
        channel.confirmSelect();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.save";

        //发送一条消息
        String msg = "Hello RabbitMQ Send confirm message!";
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());

        //添加一个确认监听
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("---------ack!-----------");
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("---------no ack!-----------");
            }
        });

    }
}

消费者:

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //创建一个connectionFactory,并进行配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");

        //通过连接工厂 创建连接
        Connection connection = connectionFactory.newConnection();

        //通过 connection 创建一个 channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.#";
        String queueName = "test_confirm_queue";

        //声明交换机和队列 然后进行绑定设置 最后制定路由key
        channel.exchangeDeclare(exchangeName, "topic", true);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

        //创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, queueingConsumer);

        while (true) {
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(msg);
        }
    }
}
Return 消息机制

Return Listener 用于处理一些不可路由的消息

我们的消息生产者,通过指定一个Exchange和Routingkey,把消息送达到某一个队列中去。然后我们的消费监听队列,进行消费处理操作。

但是在某些情况下,如果我们在发消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用Return Listener。

在基础API中有一个关键的配置项:Mandatory,如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息。

在这里插入图片描述
代码实现:

生产者:

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个connectionFactory,并进行配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");

        //通过连接工厂 创建连接
        Connection connection = connectionFactory.newConnection();

        //通过 connection 创建一个 channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_return_exchange";
        String routingKey = "return.save";
        String routingKeyError = "abc.save";

        String msg = "Hello RabbitMQ Return Message";
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                     AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                System.err.println("------heandle return--------");
                System.err.println("replyCode: " + replyCode);
                System.err.println("replyText: " + replyText);
                System.err.println("exchange: " + exchange);
                System.err.println("routingKey: " + routingKey);
                System.err.println("properties: " + basicProperties);
                System.err.println("body: " + new String(body));
            }
        });

        //channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
        channel.basicPublish(exchangeName, routingKeyError, true, null, msg.getBytes());
    }
}

消费者:

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //创建一个connectionFactory,并进行配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");

        //通过连接工厂 创建连接
        Connection connection = connectionFactory.newConnection();

        //通过 connection 创建一个 channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_return_exchange";
        String routingKey = "return.#";
        String routingKeyError = "abc.save";
        String queueName = "test_return_queue";

        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);

        channel.basicConsume(queueName, true, queueingConsumer);

        while (true) {
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(msg);
        }
    }
}

当发送的消息没有接收的队列时:
在这里插入图片描述

消费端自定义监听

上面我们接收消息都是使用while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理。

但是我们使用自定义的Consumer更加的方便,解耦性更加的强,也是在实际工作中最常用的使用方式。

创建自定义消费者:

public class MyConsumer extends DefaultConsumer {
    public MyConsumer(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.err.println("consuerTag:" + consumerTag);
        System.err.println("envelope:" + envelope);
        System.err.println("properties:" + properties);
        System.err.println("body:" + new String(body));
    }
}

消费端使用自定义消费者进行消费:

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个connectionFactory,并进行配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");

        //通过连接工厂 创建连接
        Connection connection = connectionFactory.newConnection();

        //通过 connection 创建一个 channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_consumer_exchange";
        String routingKey = "consumer.#";
        String queueName = "test_consumer_queue";

        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

        channel.basicConsume(queueName, true, new MyConsumer(channel));
    }
}
消费者限流

RabbitMQ 提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消息新的消息。

限流主要使用 void BasicQos(uint prefetchSize, ushort prefetchCount, bool global)
参数:

  • prefetchSize: 0 消息大小限制。
  • prefetchCount: 会告诉RabbitMQ不要同时给一个消费者推送多个N个消息,即一旦有N个消息没有ack,则该consumer将block掉,直到有消息ack。
  • global: true\false 是否将上面设置应用于channel。简单点说,就是上面限制是channel级别的还是consumer级别。

代码实现:

消费者配置:设置限流和签收方式设置成手动提交

        //设置每次只处理一条消息
        channel.basicQos(0, 1, false);
        //限流方式 autoAck设置为false
        channel.basicConsume(queueName, false, new MyConsumer(channel));

自定义消费者:

public class MyConsumer extends DefaultConsumer {
    private Channel channel;
    
    public MyConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.err.println("consuerTag:" + consumerTag);
        System.err.println("envelope:" + envelope);
        System.err.println("properties:" + properties);
        System.err.println("body:" + new String(body));
        
        //ack 第二个参数 是否批量提交
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
}
消费端ACK

消费端可以进行手工端ACK和NACK

消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿。也可以通过手工NACK,让消息重新投递和消费,可以记录最大重试次数。

消费端端重回队列

消费端重回队列是为了对没有处理成功对消息,把消息重新会投递给Broker。

一般我们在实际应用中,都会关闭重回队列,也就是设置为False。

//第三个参数 true代表重回队列 false 代表不重回队列
if((Integer)properties.getHeaders().get("num") == 0) {
    //第三个参数 true代表重回队列
    channel.basicNack(envelope.getDeliveryTag(), false, true);
} else {
    //ack 第二个参数 是否批量提交
    channel.basicAck(envelope.getDeliveryTag(), false);
}
TTL队列/消息

TTL

  • TTL是Time To Live的缩写,也就是生存时间。
  • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定。
  • RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除。

TTL分成两种:

  1. 队列级别:加入队列中的所有消息都有TTL时间,在控制台上可以加。
  2. 消息级别:在发消息时指定 new AMQP.BasicProperties.Builder().expiration("10000").build();
死信队列

DLX Dead-Letter-Exchange

利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。

消息变成死信有以下几种情况

  • 消息被拒绝且requeue=false
  • 消息TTL过期
  • 队列达到最大长度

DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。

当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。

可以监听这个队列中消息做相应的处理。

设置死信队列

首先需要设置死信队列的exchange和queue,然后进行绑定:

  • Exchange: dlx.exchange
  • Queue: dlx.queue
  • RoutingKey: #

然后我们进行正常声明交换机、队列、绑定,只不过我们需要在队列加上一个参数即可:arguments.put("x-dead-letter-exchange","dlx.exchange")

这样消息在过期、requeue、队列在达到最大长度时,消息就可以直接路由到死信队列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值