RabbitMq快速入门(初级<------>高级特性)

原文链接:RabbitMq快速入门(初级<——>高级特性) – 编程屋

目录

1 MQ的相关概念

1.1 mq的相关概念

1.2 mq的作用

2 Mq的分类

3 RabbitMq

3.1 RabbitMq概念

3.2 RabbitMq中的四大核心概念

3.3 RabbitMq的工作原理

3.4 RabbitMq的工作模式

3.4.1 简单模式(Simple)

3.4.2 工作队列模式

 3.4.3 发布订阅模式

3.4.4 路由模式 

3.4.5  主题模式(通配符模式)

3.4.6 RPC模式(暂不了解)

4 SpringBoot整合RabbitMq

4.1 生产者相关配置

4.2 消费者相关配置

5 RabbitMq的高级特性

5.1 消息的可靠投递

5.1.1 消息确认(confirmCallBack)

5.1.2 消息回退(returnCallback)

5.2  延迟队列

5.2.1 延时队列初步实现

5.2.2 动态指定延时消息

5.2.3 rabbitmq_delayed_message_exchange插件


思考:

        如果我们的接口响应比较慢的情况会导致我们的客户端比较超时,那么就会引起客户端同步进行阻塞,那么当大量的请求进来,就有可能触发错误。那我们通过消息对列(MQ),可以处理异步请求,从而缓解系统的压力,将那些不需要同步处理并且耗时比较长的业务逻辑由消息对列进行异步处理,从而减少程序的响应时间。

1 MQ的相关概念

1.1 mq的相关概念

mq(message queue)本质上是个队列,遵循先入先出原则,只不过队列中存储的内容是消息而已,还是一种跨进程的通信机制,用于上下游传递消息。使用了mq之后,消息发送发只需要依赖mq,不用依赖其他服务,能够很好的实现解耦。

1.2 mq的作用

1 流量消峰

        为了防止订单太多导致订单系统崩溃,于是给订单系统设置了超过多少访问就不能够再次下单(比如10000单),但是在节假日高峰时,,订单的数量远远超过了这个值,那么这个时候,超过10000单后其他用户下的订单就不能够下单了,这种情况是不允许的。所以只需要将多余的订单放入mq中,让他依次来处理,这个时候虽然用户可能下单十几秒后才收到下单成功的消息,但是总比不能下单好。

2 应用解耦

        比如一个电商应用为例:里面包含了订单系统,物流系统,支付系统,库存系统。如果订单系统与它们直接耦合的话,那么当其中的一个系统发生错误,那么就会下单失败。但是将订单系统中的消息放到队列中,队列再将消息发送给其他系统,那么即是其他系统暂时出错了,由于我们的消息在队列中,那么等到系统修复好之后正常去消费在mq中的消息就可以了。

3 异步处理

        如果一个服务A调用服务B需要很长时间,但是A不知道B什么时候会执行完成。之前有两种方式,A过一段时间去查询B看B是否完成。或者A提供一个callback Api,B执行完成之后执行调用Api通知A服务。但这两种方式都不是很优雅。于是可以使用mq,当A调用B服务后,当B处理完成后,会发送一条消息给mq,mq将此消息转给A,这样A就不用循环查询B的api,也不用提供回调接口了。A服务还能及时的得到异步处理成功的消息。

2 Mq的分类

1 ActiveMQ

优点:单机吞吐量高,时效性ms级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据

缺点:官方维护少,高吞吐量场景少用

2 Kafka

一款为大数据而生的消息中间件,以其百万级的TPS而出名,在数据采集,传输,存储过程中发挥着举足轻重的作用

优点:性能卓越,单机写入TPS在百万条/秒,最大的优点就是吞吐量高。时效性ms级可用性非常高,kafka是分布式的,一个数据写多个副本,少数机器宕机不会丢失数据,不会导致不可用,消费者采用pull的方式获取消息,消息有序,通过控制保证所有的消息能够被消费且近被消费一次

缺点:kafka超过64个队列/分区,Load会发生明显的飙高现象,队列越高,load越高,发送消息响应时间变长,社区更新较慢。

3 RocketMQ

RocketMQ出自阿里的开源产品,用java语言实现。被阿里广泛应用在订单,交易,充值,流计算,消息推送,日志处理,binlog日志分发等场景。

优点:单机吞吐量十万级,可用性十万级,分布式架构,消息可以做到0丢失,扩展性好,支持十亿级别的消息堆积,不会因为堆积导致性能下降,源码是java,我们可以自己阅读源码,定制自己的mq

缺点:支持的客户端语言不多,目前是java及c++,社区活跃度一般

4 RabbitMq

07年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可用的企业消息系统,是当前最主流的消息中间件系统

优点:由于erlang语言的高并发特性,性能较好;吞吐量达到万级,功能比较完善,支持多种语言,社区活跃多高。

缺点:商用版需要收费

3 RabbitMq

3.1 RabbitMq概念

RabbitMq是一个消息中间件:它接收并转发消息。可以相当于一个快递站点,当要发送一个包裹时,将包裹放到快递站,由快递员送到指定地点。RabbitMq就相当于一个快递站,但它与快递站不同的是,它不处理快件而是接收,存储和转发消息。

3.2 RabbitMq中的四大核心概念

生产者:产生数据发送消息的程序

交换机:交换机是RabbitMq非常重要的一个组件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须知道如何处理到它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列中,还是将消息丢弃,这个是由交换机的类型来确定的。

队列:队列是RabbitMq中使用的一种数据结构,尽管消息流经RabbitMq和应用程序,但他们只能存储到队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个很大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以从一个队列中接收数据,这就是使用队列的方式。

消费者:消费者和接受者具有相似的含义。消费者大多时候是一个等待消费的程序,消费者和消息中间件很多时候并不在同一个机器上。同一个应用程序既可以生产者又可以是消费者。

3.3 RabbitMq的工作原理

Broker:接收和分发消息的应用,RabbitMq Server就是Message Broker

Virtual:出于多租户和安全因素的设计,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMq server提供服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue

Connection:publish和consumer和broker之间的TCP连接

Channel:如果每一次访问都建立一个Connection。在消息量大时建立TCP Connection的开销将是巨大的,效率也很低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独份channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel是完全隔离的。Channel作为轻量级的Connection极大的减少了操作系统建立TCP connection的开销

Exchane:message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到对列中去。常用的        类型有:direct,topic,and fanout。

3.4 RabbitMq的工作模式

3.4.1 简单模式(Simple)

  • P:生产者,将消息直接发送给队列
  • C1,C2:消费者,从队列中直接消费
  • Work queues(图中红色部分):消息队列

生产者:

 // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        //5 创建队列
        /**
         *  queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
         * @参数1 queue:队列名称
         * @参数2 durable:是否持久化
         * @参数3 exclusive: 1 是否独占。只能有一个消费者监听这队列 2当connection关闭时,是否删除队列
         * @参数4 autoDelete:是否自动删除。当没有connection时,自动删除掉
         * @参数5 arguments:参数
         */
        //如果该队列名称不存在时,则会自动创建
        channel.queueDeclare("hello_world",true,false,false,null);

        /**
         * basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
         * exchange:交换机名称。简单模式下交换机会使用默认的 ""
         * routingKey:路由名称
         * props:配置信息
         * body: 发送消息数据
         */
        String body = "我要发送消息啦。。。。。。";
        channel.basicPublish("","hello_world",null,body.getBytes());
        //7 释放资源
        channel.close();
        connectionFactory.clone();

消费者:

  // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        //5 创建队列
        /**
         *  queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
         * @参数1 queue:队列名称
         * @参数2 durable:是否持久化
         * @参数3 exclusive: 1 是否独占。只能有一个消费者监听这队列 2当connection关闭时,是否删除队列
         * @参数4 autoDelete:是否自动删除。当没有connection时,自动删除掉
         * @参数5 arguments:参数
         */
        //如果该队列名称不存在时,则会自动创建
        channel.queueDeclare("hello_world",true,false,false,null);

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
            }
        };
        channel.basicConsume("hello_world",true,consumer);
    }

运行结果: 

3.4.2 工作队列模式

  • P:生产者,将消息直接发送给队列
  • C1,C2:消费者,从队列中直接消费
  • Work queues(图中红色部分):与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列里的消息
  • 应用场景:对于任务过重或者任务较多情况使用工作队列可以提高处理任务的速度
  • 注意:在这种模式下,这两个消费者属于竞争关系,一条消息只能被一个消费者消费到。

生产者:

public static void main(String[] args) throws IOException, TimeoutException {
        // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        //5 创建队列
        /**
         *  queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
         * @参数1 queue:队列名称
         * @参数2 durable:是否持久化
         * @参数3 exclusive: 1 是否独占。只能有一个消费者监听这队列 2当connection关闭时,是否删除队列
         * @参数4 autoDelete:是否自动删除。当没有connection时,自动删除掉
         * @参数5 arguments:参数
         */
        //如果该队列名称不存在时,则会自动创建
        channel.queueDeclare("world_queues",true,false,false,null);

        /**
         * basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
         * exchange:交换机名称。简单模式下交换机会使用默认的 ""
         * routingKey:路由名称
         * props:配置信息
         * body: 发送消息数据
         */
        for (int i = 1; i <= 10; i++) {
            String body = i+"我要发送消息啦。。。。。。";
            channel.basicPublish("","world_queues",null,body.getBytes());
        }
        //7 释放资源
        channel.close();
        connectionFactory.clone();
    }

消费者1:

   // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        //5 创建队列
        /**
         *  queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
         * @参数1 queue:队列名称
         * @参数2 durable:是否持久化
         * @参数3 exclusive: 1 是否独占。只能有一个消费者监听这队列 2当connection关闭时,是否删除队列
         * @参数4 autoDelete:是否自动删除。当没有connection时,自动删除掉
         * @参数5 arguments:参数
         */
        //如果该队列名称不存在时,则会自动创建
        channel.queueDeclare("world_queues",true,false,false,null);

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body"+new String(body));
            }
        };
        channel.basicConsume("world_queues",true,consumer);
    }

消费者2(与消费者1代码一样就不在粘贴)

结果:

总共发了十条消息:消费者1消费了第1,3,5,7,9条消息

消费者2消费了第2,4,6,8,10条消息

 3.4.3 发布订阅模式

  • P: 生产者,也就是要发送消息的程序,但是不在发送到队列中,而是发送给交换机
  • C:消费者,消息的接受者,等待消费消息
  • Queue:消息队列,接收消息,缓存消息
  • Exchange:交换机。一方面接收生产者发送的消息。另一方面,知道如何处理消息,是将这些消息推送到特定队列还是推送到多个队列中,还是将消息丢弃,这些都是由Exchange的类型来确定的。Exchange有以下3种类型:

                Fanout:广播,将消息交给所有绑定到交换机的队列

                Direct:定向,把消息交给符合指定routing key的队列

                Topic:通配符,把消息交给符合routing pattern(路由模式)的队列

  • 注意:Exchange只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失

案例:用Exchange中的Fanout模式将消息发送到每个队列中

生产者:

    // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        /**
         * exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
         * exchange 交换机名称
         * type 交换机类型
         *       direct:定向
         *       fanout:扇形,发送消息到每一个与之绑定的队列
         *       topic:通配符的方式
         *       hearders:参数匹配
         * durable 是否持久化
         * autoDelete自动删除
         * internal 内部使用 一般用false
         * arguments 参数
         */
        String exchangeName = "test_fanout";
        //5 创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
        //6 构建队列
        String queueName1 = "test_fanout_queue1";
        String queueName2 = "test_fanout_queue2";
        channel.queueDeclare(queueName1,true,false,false,null);
        channel.queueDeclare(queueName2,true,false,false,null);
        // 7 绑定队列和交换机
        /**
         * queueBind(String queue, String exchange, String routingKey)
         * queue 绑定队列名称
         * exchange 交换机名称
         * routingKey 路由键,绑定规则
         *  如果交换机的类型为fanout,routingKey设置为""
         *
         */
        channel.queueBind(queueName1,exchangeName,"");
        channel.queueBind(queueName2,exchangeName,"");

        String body = "日志信息。。。。。";
        //8 发送消息
        channel.basicPublish(exchangeName,"",null,body.getBytes());
        //9 释放资源
        channel.close();
        connection.close();

消费者1:

  // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_fanout_queue1";
        String queueName2 = "test_fanout_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息打印到控制台。。。。。。");
            }
        };
        channel.basicConsume(queueName1,true,consumer);

消费者2(代码与消费者1基本相同,只是消费的队列不同):

    // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_fanout_queue1";
        String queueName2 = "test_fanout_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息保存到数据库。。。。。。");
            }
        };
        channel.basicConsume(queueName2,true,consumer);

测试结果:

消费者1控制台:

 消费者2控制台:

3.4.4 路由模式 

  • P: 生产者,也就是要发送消息的程序,但是不在发送到队列中,而是发送给交换机
  • C:消费者,消息的接受者,等待消费消息
  • Queue:消息队列,接收消息,缓存消息
  • Exchange:交换机。

注意:

        1)队列与交换机的绑定不能是任意绑定,而是要指定一个RoutingKey(路由key)

        2)消息的发送方向在向Exchange发送消息时,也必须指定消息的RoutingKey

        3)Exchange不再将消息发送给每一个绑定的队列,而是根据消息的路由keu去判断,只有队列的路由key和消息的路由key

案例:将不同级别的日志消息输出到不同的地方,以上图为例C1消费者接受error级别的消息,C2消费者接受info,error,warning级别的消息。为了更好的演示路由模式的效果将消息发送时指定的路由key为info,这样C1消费者就接受不到消息,只有C2消费者能接受到消息。因为只有C2消费者对应的队列的路由key与消息指定的路由key是相同的。

生产者代码:

 // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        /**
         * exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
         * exchange 交换机名称
         * type 交换机类型
         *       direct:定向
         *       fanout:扇形,发送消息到每一个与之绑定的队列
         *       topic:通配符的方式
         *       hearders:参数匹配
         * durable 是否持久化
         * autoDelete自动删除
         * internal 内部使用 一般用false
         * arguments 参数
         */
        String exchangeName = "test_direct";
        //5 创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,false,null);
        //6 构建队列
        String queueName1 = "test_direct_queue1";
        String queueName2 = "test_direct_queue2";
        channel.queueDeclare(queueName1,true,false,false,null);
        channel.queueDeclare(queueName2,true,false,false,null);
        // 7 绑定队列和交换机
        /**
         * queueBind(String queue, String exchange, String routingKey)
         * queue 绑定队列名称
         * exchange 交换机名称
         * routingKey 路由键,绑定规则
         *  如果交换机的类型为fanout,routingKey设置为""
         *
         */
        channel.queueBind(queueName1,exchangeName,"error");
        channel.queueBind(queueName2,exchangeName,"info");
        channel.queueBind(queueName2,exchangeName,"error");
        channel.queueBind(queueName2,exchangeName,"warning");

        String body = "日志信息。。。。。";
        //8 发送消息
        channel.basicPublish(exchangeName,"info",null,body.getBytes());
        //9 释放资源
        channel.close();
        connection.close();

消费者1代码:

  // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_direct_queue1";
        String queueName2 = "test_direct_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息打印到控制台。。。。。。");
            }
        };
        channel.basicConsume(queueName2,true,consumer);
    }

消费者2代码:

 // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_direct_queue1";
        String queueName2 = "test_direct_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息存储到数据库 。。。。。。");
            }
        };
        channel.basicConsume(queueName1,true,consumer);

测试结果:

        1)消费者1控制台(没有信息输出,因为队列绑定的路由key和消息发送时指定的路由key不同):

 2)消费者2控制台(信息输出,因为队列绑定的路由key和消息发送时指定的路由key相同同)

3.4.5  主题模式(通配符模式)

  • P: 生产者,也就是要发送消息的程序,但是不在发送到队列中,而是发送给交换机
  • C:消费者,消息的接受者,等待消费消息
  • Queue:消息队列,接收消息,缓存消息
  • Exchange:交换机。

由路由模式中的案例可以发现,只要指定的路由key就可以将消息发送到指定队列,但有时候key单一的指定拓展性不是很强,所以我们可以通过通配符来指定我们的路由key,只要让它符合我们设置的通配符格式,就可以实现相应的效果,更加灵活。

通配符规则:

   #:匹配一个或多个词

   *:匹配不多不少恰好1个词

生产者代码:

  // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();
        /**
         * exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
         * exchange 交换机名称
         * type 交换机类型
         *       direct:定向
         *       fanout:扇形,发送消息到每一个与之绑定的队列
         *       topic:通配符的方式
         *       hearders:参数匹配
         * durable 是否持久化
         * autoDelete自动删除
         * internal 内部使用 一般用false
         * arguments 参数
         */
        String exchangeName = "test_topic";
        //5 创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,false,null);
        //6 构建队列
        String queueName1 = "test_topic_queue1";
        String queueName2 = "test_topic_queue2";
        channel.queueDeclare(queueName1,true,false,false,null);
        channel.queueDeclare(queueName2,true,false,false,null);
        // 7 绑定队列和交换机
        /**
         * queueBind(String queue, String exchange, String routingKey)
         * queue 绑定队列名称
         * exchange 交换机名称
         * routingKey 路由键,绑定规则
         *  如果交换机的类型为fanout,routingKey设置为""
         *
         */
        channel.queueBind(queueName1,exchangeName,"#.error");
        channel.queueBind(queueName1,exchangeName,"order.*");
        channel.queueBind(queueName2,exchangeName,"*.*");

        String body = "日志信息。。。。。";
        //8 发送消息
        channel.basicPublish(exchangeName,"order.info",null,body.getBytes());
        //9 释放资源
        channel.close();
        connection.close();

消费者1代码:

 // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_topic_queue1";
        String queueName2 = "test_topic_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息存储到数据库 。。。。。。");
            }
        };
        channel.basicConsume(queueName1,true,consumer);

消费者2代码:

  // 1 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2 设置参数
        connectionFactory.setHost("124.221.89.80");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        //3 创建连接
        Connection connection = connectionFactory.newConnection();
        //4创建队列
        Channel channel = connection.createChannel();

        String queueName1 = "test_topic_queue1";
        String queueName2 = "test_topic_queue2";

        //接收消息
        /**
         * basicConsume(String queue, DeliverCallback deliverCallback, CancelCallback cancelCallback)
         * queue:队列名称
         * deliverCallback:是否自动确认
         * cancelCallback 回调函数
         */
        Consumer consumer = new DefaultConsumer(channel){

            /**
             * 当收到消息后会自动执行该方法
             * @param consumerTag 标识
             * @param envelope 获取一些信息,交换机,路由
             * @param properties 配置信息
             * @param body 数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag:"+consumerTag);
//                System.out.println("Exchange:"+envelope.getExchange());
//                System.out.println("RoutingKey:"+envelope.getRoutingKey());
//                System.out.println("properties:"+properties);
                System.out.println("body"+new String(body));
                System.out.println("将日志信息打印到控制台 。。。。。。");
            }
        };
        channel.basicConsume(queueName2,true,consumer);

测试结果:

        1)消费者1控制台

        2)消费者2控制台

 总结:Topic主体模式可以实现Pub/Sub发布订阅模式和Routing路由模式的功能,只是Topic在配置routing Key的时候可以使用通配符,显得更加灵活。

3.4.6 RPC模式(暂不了解)

4 SpringBoot整合RabbitMq

4.1 生产者相关配置

步骤一:创建springboot工程

步骤二:引入springboot整合rabbitmq依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

步骤三:编写相关配置文件

spring:
  rabbitmq:
    host: 124.221.89.80
    port: 5672
    username: guest
    password: guest
    virtual-host: /

步骤四:编写配置类

package com.liubujun.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;



/**
 * @Author: liubujun
 * @Date: 2022/3/22 20:03
 */

@Configuration
public class RabbitMqConfig {

    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    /**
     * 1 交换机
     *
     * @return
     */
    @Bean("bootExchane")
    public Exchange bootExchane() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 2 队列
     *
     * @return
     */
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    /**
     * 队列和交换机绑定关系
     * bind绑定队列
     * to交换机
     * with路由key
     * 没有参数noargs()
     */
    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchane") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }

    //3队列和交换机绑定关系
}

步骤五:发送消息

package com.liubujun.demo;

import com.liubujun.config.RabbitMqConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class RabbitmqSpringbootApplicationTests {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {

        rabbitTemplate.convertAndSend(RabbitMqConfig.EXCHANGE_NAME, "boot.hahaha", "你好啊");
    }

}

4.2 消费者相关配置

步骤一:创建springboot工程

步骤二:引入相关依赖(与生产者相同)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

步骤三:编写相关配置文件(与生产者相同)

spring:
  rabbitmq:
    host: 124.221.89.80
    port: 5672
    username: guest
    password: guest
    virtual-host: /

步骤四:编写相关监听类并接受消息

package com.liubujun.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @Author: liubujun
 * @Date: 2022/3/22 20:41
 */

@Component
public class RabbitmqListener {

    @RabbitListener(queues = "boot_queue")
    public void ListenerQueue(Message message){
        System.out.println(new String(message.getBody()));
    }


}

测试结果(先启动生产者发送消息,再启动消费者监听消息)

消息已经成功被消费 

5 RabbitMq的高级特性

5.1 消息的可靠投递

在生产环境中,由于一些原因导致rabbitmq消息投递失败,以至于消息丢失,需要手动处理和恢复,所以RabbitMq提供了两种方式用来控制消息投递的可靠性。

  • confirm 确认模式
  • return 退回模式

rabbitMq的整个投递路径为:

 rabbitMq为了确保消息的准确投递:

  •         消息从producer到exchange则会返回一个confirmCallBack。
  •         消息从exchange到queue投递失败则会返回一个returnCallback。

5.1.1 消息确认(confirmCallBack)

配置类:

package com.liubujun.config;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Author: liubujun
 * @Date: 2022/3/23 21:13
 */

@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        //注入
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1 生产者发消息发消息 交换机收到了 回调
     * @param correlationData 保存回调消息的id及相关信息
     * @param ack 交换机收到消息 true
     * @param cause
     *
     * 2 生产者发消息发消息 交换机没有收到 回调
     *  * @param correlationData 保存回调消息的id及相关信息
     *  * @param ack 交换机收到消息 false
     *  * @param cause 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack){
            System.out.println("交换机收到消息id:"+id);
        }else {
            System.out.println("未收到消息原因:"+cause);
        }

    }
}

生产者:

package com.liubujun.controller;

import com.liubujun.config.ConfirmConfig;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: liubujun
 * @Date: 2022/3/22 22:04
 */


@RestController
@RequestMapping("/confirm")
public class ProducerController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
  System.out.println("发送消息内容:"+message);
    }
}

消费者:

package com.liubujun.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @Author: liubujun
 * @Date: 2022/3/22 20:41
 */

@Component
public class RabbitmqListener {


    @RabbitListener(queues = "confirm_queue")
    public void ListenerQueueConfirm(Message message){
        System.out.println("接收到队列confirm_queue的消息:"+new String(message.getBody()));
    }


}

配置文件:

在配置文件中需要添加:

spring.rabbitmq.publisher-confirm-type=correlated

  • NONE:禁用发布模式,是默认值
  • CORRELATED:发布消息成功到交换机后会触发回调方法
  • SIMPLE:

        效果1:与CORRELATED一样触发回调方法

        效果2:在发布消息成功后使用rabbitTemplate调用waitForConfirms或者waitForConfirmOrDie方法等待broker返回发送结果,根据返回结果判断下一步的逻辑,需注意的是waitForConfirmOrDie方法返回结果返回false则会关闭channel,则接下来无法发送消息到broker

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated

启动项目并访问:

情况一(收到消息):

提供者控制台:

消费者控制台:

情况二(收不到消息):将交换机名字写错,让它收不到消息(其他代码不变)

 继续按照上述方式访问:

 将错误原因粘贴出来(channel error):

未收到消息原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm_exchangekkk' in vhost '/', class-id=60, method-id=40)

这是消发送到交换机中成功和失败的演示,但是还有一种情况就是如果消息从交换机到队列这一过程中丢失了我们的生产者能知道吗?

5.1.2 消息回退(returnCallback)

制造消息从交换机到队列这一过程中丢失的场景(将路由key改掉),改成与我们的路由key不相等就好

 按照之前方式访问:

服务端控制台: 

此时我们消费端控制台没有接收到任何消息

 发现此时我们的生产者是不知道我们的消息丢失了的同样消费者也接收不到消息,但是在生产上我们的消息是不能够丢失的,那如何将我们的消息找回来呢?

application.yml:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true

相关配置类:

package com.liubujun.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Author: liubujun
 * @Date: 2022/3/23 21:13
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        //注入
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1 生产者发消息发消息 交换机收到了 回调
     * @param correlationData 保存回调消息的id及相关信息
     * @param ack 交换机收到消息 true
     * @param cause
     *
     * 2 生产者发消息发消息 交换机没有收到 回调
     *  * @param correlationData 保存回调消息的id及相关信息
     *  * @param ack 交换机收到消息 false
     *  * @param cause 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack){
            System.out.println("交换机收到消息id:"+id);
        }else {
            System.out.println("未收到消息原因:"+cause);
        }

    }

    /**
     * 只有在消息传递过程中不可达到目的地时将消息返回给生产者
     * 只有不可达目的地的时候才进行回退
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info("消息:{},被交换机{}退回,退回原因:{},路由key:{}",returnedMessage.getMessage(),returnedMessage.getExchange()
                ,returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
    }
}

按照之前方式访问:

服务提供者控制台(已经发现消息被退回):

发送消息内容:你好啊
2022-03-25 21:57:56.032  INFO 12168 --- [nectionFactory1] com.liubujun.config.MyCallBack           : 消息:(Body:'你好啊' MessageProperties [headers={spring_returned_message_correlation=1}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]),被交换机confirm_exchange退回,退回原因:NO_ROUTE,路由key:key1222
交换机收到消息id:1

5.2  延迟队列

项目结构:

含义:延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后才会被消费

5.2.1 延时队列初步实现

但rabbitMq中并不直接支持延迟队列的使用。但是可以通过rabbitMq的另外两个特性来实现这个功能。TTL+死信队列组合实现延迟队列的效果

  • TTL :全称Time To Live(存活时间/过期时间)。当消息达到存活时间后,还没有被消费,会被自动清除。RabbitMq中可以对消息设置过期时间,也可以对整个队列设置过期时间。
  • 死信队列:英文缩写 DLX。Dead Letter Exchange(死信交换机),当消息成为Dead Message后,可以被重新发送到另外一个交换机,这个交换机就是DLX。

场景:

定义两个延时队列:假始QA延时2s,QB延时20s,这样到一定时间延时消息没有被消费就会被发送到死信交换机并且路由到私信队列中。

 配置类:

package com.liubujun.rabbitmqspringbootdemo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 15:21
 */

@Component
public class TtlQueueConfig{

    //普通交换机名称
    public static final String X_EXCHANGE = "X";

    //死信交换机的名称
    public static final String Y_DEAD_LETTER_XCHANGE = "Y";

    //普通队列的名称
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";

    //死信队列的名称
    public static final String DEAD_LETTER_QUEUE = "QD";

    //声明X交换机
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    //声明Y交换机
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_XCHANGE);
    }

    //声明普通队列为10s
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> arguments  = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_XCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置ttl
        arguments.put("x-message-ttl",10000);
        return  QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    //声明普通队列为40s
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> arguments  = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_XCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置ttl
        arguments.put("x-message-ttl",40000);

        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    //声明死信队列
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    //绑定
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    //绑定
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    //绑定
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }

}

 生产者:

package com.liubujun.rabbitmqspringbootdemo.controller;

import com.liubujun.rabbitmqspringbootdemo.config.TtlQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 16:21
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SeneMsgConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        log.info("当前时间发送:{},发送一条消息给两个TTL队列:{}",new Date().toString(),message);
        rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE,"XA","消息为来自ttl为10s的队列:"+message);
        rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE,"XB","消息为来自ttl为40s的队列:"+message);
    }

}

消费者:

package com.liubujun.rabbitmqspringbootdemo.consumer;


import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 16:32
 */

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel){
        log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(),new String(message.getBody()));
    }
}

application.yml:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

访问:

控制台输出:

可以发现一条消息分别在10s和40s后分别被接收到。这样就实现了这个延时队列功能。

5.2.2 动态指定延时消息

        但是可以对以上的代码做一个优化,因为上面的队列只能实现指定的延时时间,如果我想实现一个延时半小时的,那么有需要重写去定一个队列,想实现一个延时1小时的,又得需要重新定义一个队列,所以如何动态的去指定延时消息呢?所以可以根据生产者发消息时来指定消息的延时时间。增加代码如下:

生产者改动:

package com.liubujun.rabbitmqspringbootdemo.controller;

import com.liubujun.rabbitmqspringbootdemo.config.TtlQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 16:21
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SeneMsgConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        log.info("当前时间发送:{},发送一条消息给两个TTL队列:{}",new Date().toString(),message);
        rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE,"XA","消息为来自ttl为10s的队列:"+message);
        rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE,"XB","消息为来自ttl为40s的队列:"+message);
    }

    @GetMapping("/sendMessage/{message}/{ttlTime}")
    public void sendExpirationMessage(@PathVariable String message,@PathVariable String ttlTime){
        log.info("当前时间发送:{},发送一条时长:{},消息给TTL队列QC:{}",new Date().toString(),ttlTime,message);
        rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE,"XC","消息为来自QC的队列:"+message,msg->{
            //发送消息的时候延时时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }


}

配置类改动:

package com.liubujun.rabbitmqspringbootdemo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 15:21
 */

@Component
public class TtlQueueConfig{

    //普通交换机名称
    public static final String X_EXCHANGE = "X";

    //死信交换机的名称
    public static final String Y_DEAD_LETTER_XCHANGE = "Y";

    //普通队列的名称
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    public static final String QUEUE_C = "QC";

    //死信队列的名称
    public static final String DEAD_LETTER_QUEUE = "QD";

    //声明X交换机
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    //声明Y交换机
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_XCHANGE);
    }

    //声明普通队列为10s
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> arguments  = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_XCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置ttl
        arguments.put("x-message-ttl",10000);
        return  QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    //声明普通队列为40s
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> arguments  = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_XCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置ttl
        arguments.put("x-message-ttl",40000);

        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    @Bean("queueC")
    public Queue queueC(){
        Map<String, Object> arguments  = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_XCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置ttl
        return  QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }

    //声明死信队列
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    //绑定
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    //绑定
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    //绑定
    @Bean
    public Binding queueCBindingY(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

    //绑定
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }



}

连续发送两个请求:一个延时20秒,一个延时2s

 控制台输出结果:

 我们可以观察下输出顺序,发现延迟20s的消息和延时2s的消息是在同一时刻输出的,这明显不符合我们正常的需求,肯定是延时时间短的先输出。

5.2.3 rabbitmq_delayed_message_exchange插件

所以如果使用在消息属性上设置TTL的方式,消息可能不会按时“死亡”,因为rabbitmq只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时间很长,第二个消息的延时消息很短,第二个消息并不会得到优先执行。

那么如何解决这个问题呢?

rabbitmq中提供一个插件:rabbitmq_delayed_message_exchange,将其下载到RabbitMq的插件目录中。

下载地址:Community Plugins — RabbitMQ

 可能有些同学点击进去之后找不到在哪下载,点击进入如下页面

 点击进入如下页面,下载红框标注的文件

将其下载下来复制到rabbitmq中的plugins目录下,在sbin目录下执行如下命令:

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

如何看插件是否安装成功呢?如果执行没有报错的话,可以打开rabbitmq的控制台

 如果Exchange目录下,交换机类型有红框标注的类型,就说明安装成功。这个时候也说明延时的对象也从刚开始的队列转变成了交换机。

基于死信队列的情况下(延时的对象是队列):

 基于插件的情况下(延时的对象是交换机):

 于是我们用交换机是这种方式再来测试看是否还会出现上述的情况:

配置类:

package com.liubujun.rabbitmqspringbootdemo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sun.awt.CustomCursor;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 22:16
 */

@Configuration
public class DelayedQueueConfig {

    //队列
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";

    //交换机
    public static final String DELAYED_EXCHANGE_NAME = "delayed.queue";

    //routingKey
    public static final String DELAYED_ROUTING_KEY = "delayed.queue";



    //声明队列
    @Bean
    public Queue delayedQueue(){
    return QueueBuilder.durable(DELAYED_QUEUE_NAME).build();
    }

    //声明基于插件的交换机
    @Bean
    public CustomExchange delayedExchange(){
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-delayed-type","direct");

        /**
         * 交换机名称
         * 交换机的类型
         * 是否需要持久化
         * 是否需要自动删除
         */
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
    }

    //绑定
    @Bean
    public Binding delayedQueueBindingDelay(@Qualifier("delayedQueue") Queue delayedQueue,
                                            @Qualifier("delayedExchange") CustomExchange delayedExchange){
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }



}

生产者:

    @GetMapping("/sendDelayMessage/{message}/{delayTime}")
    public void sendDelayMessage(@PathVariable String message,@PathVariable Integer delayTime){
        log.info("当前时间发送:{},发送一条时长:{},毫秒消息给给延时队列:{}",new Date().toString(),delayTime,message);
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg->{
            //发送消息的时候延时时长
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
    }

消费者:

package com.liubujun.rabbitmqspringbootdemo.consumer;

import com.liubujun.rabbitmqspringbootdemo.config.DelayedQueueConfig;
import com.sun.deploy.security.DeployURLClassLoader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author: liubujun
 * @Date: 2022/3/26 22:44
 */

@Slf4j
@Component
public class DelayQueueConsumer {

    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayQueue(Message message){
        log.info("当前时间:{},收到队列消息:{}",new Date(),new String(message.getBody()));
    }
}

连续发送两条消息:第一条时长20s,第二条时长2s,如果第二条消息先被消费,说明成功。

控制台输出消息:

 发现是第二条消息先被消费,符合要求

以上只是部分内容,为了维护方便,本文已迁移到新地址:RabbitMq快速入门(初级<——>高级特性) – 编程屋

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值