快速入门RabbitMq(第二篇)

一.发布确认

  • 问题产生:虽然实现持久化主要是设置队列和消息持久化,但还可能出现一种情况导致持久化失败,就是实现以上两种持久化之前就宕机,从而造成消息丢失,即未真正实现持久化
  • 解决问题:生产者发送消息给Rabbitmq队列后,该队列只在真正实现了将消息保存到磁盘上(持久化)才通知生产者实现持久化成功,整个过程就叫发布确认
  • 开启发布确认方法:默认未开启,具体实现如下:
channel.confirmSelect();

1.单个确认发布

  • 概念:它是一种同步确认发布方式,即发布一个消息后,只有它被确认发布了,后续的消息才能继续发布
  • 缺点:发布速度慢,因为如果没有确认发布的消息会阻塞后续消息的发布
  • 简单测试(仅为代码功能重点部分)
        int publicCount = 1000;//设置总的消息数
        channel.confirmSelect();//开启发布确认
        for(int i = 0; i < publicCount; i++){
            String message = "" + i;
            channel.basicPublish("", queueName, null, message.getBytes());
            if(channel.waitForConfirms()){//判断单条消息是否确认发布成功
               System.out.println("确认一条成功");
            }
        }

2.批量确认发布

  • 概念:同单个确认发布一样是同步的,每发一批消息就统一确认发布一次,相对于单个确认发布提高了吞吐量,但缺点在于如果某一批中的消息出现了问题,无法准确判断是哪一个消息的问题
  • 简单测试
        int publicCount = 1000;//设置总的消息数
        int bathSize = 10;//设置单批数量
        for(int i = 0; i < publicCount; i++){
            String message = "" + i;
            channel.basicPublish("", queueName, null, message.getBytes());
            if(i % bathSize == 0){//发送的消息数每达到一批就确认发布
               channel.waitForConfirms(); 
               System.out.println("确认一条成功");
            }
        }

3.异步确认发布

在这里插入图片描述
图解:

  • 通过map表示channel给对应的消息内容排序的形式,这样做可以方便通过序号在全部信息发布完成后,分辨具体哪些消息确认发布,哪些没有
  • broker用于接收消息后,最终回复给生产者,哪些消息确认发布,哪些没有。
  • 对于生产者而言,只管发送消息即可,不用管哪些消息是否确认发布成功,因为发完信息后,还可以通过broker给生产者回复发送消息的情况,这便是异步的意思,即把发送消息和确认发布分离完成
  • 简单测试
        channel.confirmSelect();//开启确认发布
        //消息确认成功,回调函数
        ConfirmCallback ackCallback = (deliveryTag, multiple)->{
            System.out.println("确认的消息:" + deliveryTag);
        };
        //消息确认失败,回调函数
        /**
         * 参数1,消息的标记
         * 参数2,是否为批量确认
         */
        ConfirmCallback nackCallback = (deliveryTag, multiple)->{
            System.out.println("未确认的消息:" + deliveryTag);
        };
        //准备消息监听器,监听哪些消息成功确认发布,哪些没有
        /**
         * 参数1,监听哪些消息成功
         * 参数2,监听哪些消息失败
         */
        channel.addConfirmListener(ackCallback, nackCallback);
        for(int i = 0; i < 1000; i++){
            String message = "" + i;
            channel.basicPublish("", queueName, null, message.getBytes());
        }

4.处理异常未确认消息

  • 方案:把未确认的消息放到一个基于内存的能被发布线程访问的队列,这个队列在confirm callbacks与发布线程之间进行消息传递,由于发布线程和回调线程是不同的线程,则要选用多线程相关队列
  • 简单测试(存储信息队列选用ConcurrentSkipListMap):
    第一步:队列记录下所有要发送的消息
    第二步:删除已经确认的消息,剩下的就是未确认的消息
       channel.confirmSelect();
        /**
         * 线程安全有序的哈希表,适用于高并发
         * 1.将序号与信息进行关联
         * 2.批量删除条目,只有给到序号
         * 3.支持多线程
         */
        ConcurrentSkipListMap<Long, String> map = new ConcurrentSkipListMap<>();
        //消息确认成功,回调函数
        ConfirmCallback ackCallback = (deliveryTag, multiple)->{
            if(multiple){//批量处理
               //删除已经确认的消息,剩下的就是未确认的消息
               ConcurrentNavigableMap<Long, String> comfirmedMap = map.headMap(deliveryTag);
               comfirmedMap.clear();
            }else{//单个处理
                map.remove(deliveryTag);
            }
               System.out.println("确认的消息:" + deliveryTag);
        };
        //消息确认失败,回调函数
        /**
         * 参数1,消息的标记
         * 参数2,是否为批量确认
         */
        ConfirmCallback nackCallback = (deliveryTag, multiple)->{
            String message = map.get(deliveryTag);
            System.out.println("未确认的消息:" + message + "tag:" + deliveryTag);
        };
        //准备消息监听器,监听哪些消息成功确认发布,哪些没有
        /**
         * 参数1,监听哪些消息成功
         * 参数2,监听哪些消息失败
         */
        channel.addConfirmListener(ackCallback, nackCallback);

        long begin = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++){
            String message = "" + i;
            channel.basicPublish("", queueName, null, message.getBytes());
            //此处记录下所有要发送的消息
            map.put(channel.getNextPublishSeqNo(), message);
        }

二.交换机

  • 问题产生:原本每条消息只能被消费者消费一次,但如果想要实现一条消息被多个消费者消费,实现模式图如下:
    在这里插入图片描述图解:
    1.通过RoutingKey把交换机和多个队列绑定
    2.每个的队列的消息还是只能被消费一次,但由于一个交换机绑定了多个队列,故能实现消息被多个消费者消费
  • 概念:生产者发送的消息从不会直接发送到队列,而是要先经过交换机才能到达队列,即使是之前没有设置交换机,也会使用默认交换机。一方面用于接收生产者的信息,另一方面将这些消息加入队列
  • 处理方式:由交换机类型决定
    1.把消息放到特定单个队列或多个队列
    2.丢弃消息

1.类型一–扇形发布-订阅模式Fanout

  • 概念:将接收到的所有消息广播到它知道的所有队列中,即交换机和消费者是一对多的关系
  • 补充概念:临时队列,在以下代码中有创建代码,特点是取名随机,一旦断开了消费者的连接,队列会被自动删除
  • 简单测试:由于测试的两个消费者代码一样,故只给出一个消费者的代码
public class producer1 {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println("成功发送");
        }
    }
}

public class consumer1 {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        //参数1设置交换机名称,参数2设置交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //创建临时队列,并获取名称
        String queue = channel.queueDeclare().getQueue();
        channel.queueBind(queue, EXCHANGE_NAME, "");
        System.out.println("等待接收消息");
        DeliverCallback deliverCallback = (comsumerTag, message)->{
            System.out.println("接收到消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume(queue, true, deliverCallback,comsumerTag ->{});
    }
}

2.类型二–直接交换机Direct exchange(路由交换机)

  • 概念:根据起绑定作用的routingkey的不同,实现消费者的切换,即可以通过指定routingkey绑定相应的队列,使消息被特定的队列接收,效果图如下:
    在这里插入图片描述
    说明:上图的交换机绑定了q1和q2两个队列,绑定类型为direct,绑定q1的routingkey为orange,绑定q2的routingkey为black和green
  • 简单测试
public class producer1 {
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String message = scanner.next();
            //参数1:交换机名;参数2:绑定键routingkey
            channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes("UTF-8"));
            System.out.println("成功发送");
        }
    }
}

public class consumer1 {
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        //声明交换机
        //参数1:交换机名;参数2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare("console", false, false, false, null);
        //绑定队列
        //参数1:队列名;参数2:交换机名;参数3:routingkey
        channel.queueBind("console", EXCHANGE_NAME, "info");
        channel.queueBind("console", EXCHANGE_NAME, "warning");
        System.out.println("consumer1等待接收消息");
        DeliverCallback deliverCallback = (comsumerTag, message)->{
            System.out.println("consumer1接收到消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("console", true, deliverCallback,comsumerTag ->{});
    }
}

public class consumer2 {
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare("disk", false, false, false, null);
        channel.queueBind("disk", EXCHANGE_NAME, "error");
        System.out.println("consumer2等待接收消息");
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("disk", true, deliverCallback, consumerTag ->{});
    }
}

3.类型三–主题交换机topics

  • 问题产生:对于直接交换机来说,设置了routingkey后,只能绑定一个队列,如果想要同时绑定多个队列而且routingkey又各不相同,这是行不通的

  • 解决:使用topic交换机

  • 规范
    1.routingkey必须是一个单词列表,以点号分隔开
    2.替换符*(星号)可以代替一个单词;#(井号)可以代替0个或多个单词
    3.单词列表最多不能超过255个字节

  • 效果图
    在这里插入图片描述
    在这里插入图片描述

  • 注意点
    1.当一个队列绑定键为#,则这个队列将接收所有数据,像fanout
    2.如果队列绑定键中没有#和*出现,则该队列绑定类型为direct

  • 简单测试:以下代码参照上面的效果图进行

public class producer1 {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String message = scanner.next();
            //自定义参数2,可参照效果图进行验证
            channel.basicPublish(EXCHANGE_NAME, "quick.orange.fox", null, message.getBytes("UTF-8"));
            System.out.println("成功发送");
        }
    }
}

public class consumer1 {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        channel.queueDeclare("console", false, false, false, null);
        channel.queueBind("console", EXCHANGE_NAME, "lazy.#");
        channel.queueBind("console", EXCHANGE_NAME, "*.*.rabbit");
        System.out.println("consumer1等待接收消息");
        DeliverCallback deliverCallback = (comsumerTag, message)->{
            System.out.println("consumer1接收到消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("console", true, deliverCallback,comsumerTag ->{});
    }
}

public class consumer2 {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        channel.queueDeclare("disk", false, false, false, null);
        channel.queueBind("disk", EXCHANGE_NAME, "*.orange.*");
        System.out.println("consumer2等待接收消息");
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("disk", true, deliverCallback, consumerTag ->{});
    }
}

三.死信队列

  • 概念:死信,即无法被消费的消息,在某些特定的原因导致queue中的某些信息无法被消费,这样的信息如果没有后续处理,就变成死信,而死信队列就是用来存储死信的

  • 应用场景:为了保证消息数据不丢失,当消息消费发生异常,就将消息投入死信队列中,例如,用户下单成功,但在规定支付时间内未支付则会自动失效

  • 死信来源
    1.消息TTL(存活时间)过期
    2.队列满了
    3.消息被拒绝且不返回队列中

  • 结构图:帮助理解以下代码的逻辑
    在这里插入图片描述
    说明:由于normal-queue和dead-queue,dead_exchange有关联,故而可以通过以下代码的map来建立联系,这也是与之前代码最大的不同

  • 简单测试一(消息TTL过期)
    注意点一:consumer1的map的key部分是固定写法,更改会报错
    注意点二:先启动consumer1,为了创建好所需的交换机和队列,为了验证消息进入死信队列的效果,故可以关闭它了;再启动producer1进行消息发送,消息由于没有消费者,则会进入死信队列;最后开启consumer2消费死信队列
    注意点三:对于已经声明过的队列或交换机,不用声明第二次,否则会报错。例如consumer1中声明了死信队列和死信交换机,那么在producer1和consumer2中就不用再次声明

public class producer1 {
    public static final String EXCHANGE_NAME = "normal_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        //死信消息设置TTL时间 单位ms
        AMQP.BasicProperties properties =
                new AMQP.BasicProperties()
                    .builder().expiration("10000").build();
        for(int i = 1; i < 11; i++){
            String message = "info" + i;
            channel.basicPublish(EXCHANGE_NAME, "zhangsan", properties, message.getBytes());
        }
    }
}

public class consumer1 {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String DEAD_QUEUE = "dead-queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        Map<String,Object> map = new HashMap<>();
        //正常队列设置死信交换机
        map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        //设置死信Routingkey
        map.put("x-dead-letter-routing-key", "lisi");
        //声明正常队列
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
        //声明死信队列
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
        //绑定正常交换机和正常队列
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        //绑定死信交换机和死信队列
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
        };
        channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, (consumerTag)->{});
    }
}

public class consumer2 {
    public static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("等待接收消息");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
    }
}
  • 简单测试二(正常队列已满)
    注意点:测试的时候,由于以下代码大部分与测试一相同,而区别仅在于对正常队列的设置,故而要删除原来的normal-queue,再重新用consumer2创建
public class producer1 {
    public static final String EXCHANGE_NAME = "normal_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        for(int i = 1; i < 11; i++){
            String message = "info" + i;
            channel.basicPublish(EXCHANGE_NAME, "zhangsan", properties, message.getBytes());
        }
    }
}

public class consumer1 {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String DEAD_QUEUE = "dead-queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        Map<String,Object> map = new HashMap<>();
        //正常队列设置死信交换机
        map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        //设置死信Routingkey
        map.put("x-dead-letter-routing-key", "lisi");
        //设置正常队列的最大长度为6
        map.put("x-max-length", 6);
        //声明正常队列
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
        //声明死信队列
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
        //绑定正常交换机和正常队列
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        //绑定死信交换机和死信队列
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
        };
        channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, (consumerTag)->{});
    }
}

public class consumer2 {
    public static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("等待接收消息");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
    }
}
  • 简单测试三(消息被拒绝)
    注意点:测试的时候,由于以下代码大部分与测试一相同,但有也有些区别,最好删除原来的normal-queue(如果允许报错),再重新用consumer2创建
public class producer1 {
    public static final String EXCHANGE_NAME = "normal_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        for(int i = 1; i < 11; i++){
            String message = "info" + i;
            channel.basicPublish(EXCHANGE_NAME, "zhangsan", null, message.getBytes());
        }
    }
}

public class consumer1 {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String DEAD_QUEUE = "dead-queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        Map<String,Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        map.put("x-dead-letter-routing-key", "lisi");
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            String msg = new String(message.getBody(), "UTF-8");
            if(msg.equals("info5")){
                System.out.println("consumer1接收消息" + msg + "被拒绝");
                //channel.basicReject(消息标识,是否放回队列)
                //根据以上注释参数可知,此处设置被拒绝的消息不会放回正常队列,而是进入死信队列
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            }else{
                System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
                //channel.basicAck(消息标识,是否批量应答)
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            }
        };
        channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, (consumerTag)->{});
    }
}

public class consumer2 {
    public static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("等待接收消息");
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值