Rabbitmq消息中间件初步学习——第二节七种模型分析

前言

相对于其他消息中间件,Rabbitmq高可靠性应该算是不错的特点了吧

Rabbitmq应用场景
1.RabbitMQ的消息应当尽可能的小,并且只用来处理实时且要高可靠性的消息。
2.消费者和生产者的能力尽量对等,否则消息堆积会严重影响RabbitMQ的性能。
3.集群部署,使用热备,保证消息的可靠性。

本文参照官网Rabbitmq Tutorial内容对Rabbitmq进行特性分析
Rabbitmq消息中间件初步学习——第二节七种模型分析_第1张图片
目前一共七种模型,前五种常用,后两种也是刚刚接触,一边学习一边整理吧,肯定有遗漏的地方,望见谅!

一、 Hello World!

学习任何一门语言,都离不开 “Hello World!”,就是一个入门案例!Rabbitmq的 "Hello World!"指的是一种直连的方式,何为直连呢?这就不得不提Rabbitmq的一些概念了!
Rabbitmq消息中间件初步学习——第二节七种模型分析_第2张图片

Rabbitmq中在目前初步学习的阶段,最需要了解的概念就是生产者、消费者、虚拟主机、交换机、队列,当然还有集群,但这节主要是前几种!
如上图所示:

  1. 一个RabbitmqServer中是有多个虚拟主机的,每个虚拟主机中又存在多个交换机和队列
  2. 平时工作开发是以虚拟主机为基础进行的,交换机和队列是AMQP协议中的概念,引用百度百科的解释

在服务器中,三个主要功能模块连接成一个处理链完成预期的功能:
“exchange”接收发布应用程序发送的消息,并根据一定的规则将这些消息路由到“消息队列”。
“message queue”存储消息,直到这些消息被消费者安全处理完为止。
“binding”定义了exchange和message queue之间的关联,提供路由规则。

按我的理解就是,exchange对应的是生产者,queue对应的是消费者
3. 每个虚拟机都有一个默认的交换机

大概的概念就是这样,主要的就要知道,我们是基于虚拟主机进行开发的,生产者对应的是exchange,消费者对应的是queue,这样看一下Hello World模型,这个模型也是最简单的模型,在这个模型中是没有交换机的概念的,看一下官网的流程图
在这里插入图片描述
Producer和Consumer直接和queue相连接,也就是说我们Producer发送消息直接发送到queue中,其实这是一个错觉,因为Rabbitmq是基于AMQP协议开发的,怎么可能没有exchange呢,那么为什么上面的模型中没有exchange呢?

其实这是因为每个虚拟主机都有一个默认的exchange,在这个模型中,message是默认发送到默认的exchange(AMQP default)中的,然后路由到queue中,最后被Consumer接收!
这种模型也类似于一种直连的方式

下面是一个简单的案例!

1.POM坐标

		<dependency>
            <groupId>com.rabbitmqgroupId>
            <artifactId>amqp-clientartifactId>
            <version>5.10.0version>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.13version>
        dependency>

2.工具类

简单写了一个获取连接的工具类

/**
 * @program: rabbitmq-demo
 * @description: Rabbitmq工具类
 **/
public class RabbitmqConfig {

    /**
     * 创建连接mq的连接工厂对象
     */
    private static ConnectionFactory connectionFactory = new ConnectionFactory();
    static {
        // 连接rabbitmq的主机
        connectionFactory.setHost("127.0.0.1");
        // 设置连接端口号
        connectionFactory.setPort(5672);
        /**
         * 设置连接虚拟主机,虚拟主机:类似于nacos中命名空间概念
         * 举个例子:
         *      搭建一个Rabbitmq消息中间件后,此时有多组服务进行通信,原则上讲
         *      每一组服务之间是相互隔离的,也就是说,只允许A组内部服务之间进行相互通信,
         *      不允许A组和B组相互通信,这样就可以将它们划分在不同的虚拟主机中,完成这个功能
         */
        connectionFactory.setVirtualHost("/test");
        /**
         * 设置用户名
         */
        connectionFactory.setUsername("admin");
        // 设置密码
        connectionFactory.setPassword("admin");
    }

    /**
     * 获取连接对象
     * @return
     * @throws IOException
     * @throws TimeoutException
     */
    public static Connection getConnection() throws IOException, TimeoutException {
        return connectionFactory.newConnection();
    }

    /**
     * 获取连接中的通道
     * @return
     * @throws IOException
     * @throws TimeoutException
     */
    public static Channel getChannel() throws IOException, TimeoutException {
        return getConnection().createChannel();
    }
}

3.Producer

/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 **/
public class Producer {

    @Test
    public void publishing() {
        try {
            // 通过工具类获取Channel
            Channel channel = RabbitmqConfig.getChannel();
            /**
             *  声明队列
             *    queue: 队列名称 ,若队列已存在,但是参数不一致,则报错!
             *    durable:队列持久化,若为true,则rabbitmq重启后仍然存在
             *    exclusive:排它性,true:只可以此Connection连接这个queue,当当前Connection关闭时,这个queue会自动删除!
             *              并且在存在过程中,其他的Connection不可以连接这个queue,原理嘛,就是加个排它锁
             *    autoDelete:自动删除,true:当queue不再使用后自动删除;不再使用:即queue中无数据,没有其它connection连接此queue
             *    arguments:额外参数
             */
            channel.queueDeclare("hello",false,false,false,null);
            /**
             * 发布消息
             *  exchange: exchange名称
             *  routingKey:路由名称,不一定是某个对应的queue名称,当然肯定是需要匹配到某个queue的
             *  props:额外配置
             *  body:消息体
             */
            channel.basicPublish("","hello",null,"hello rabbitmq".getBytes());
            // 关闭channel
            channel.close();
            // 关闭Connection
            channel.getConnection().close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 测试队列持久化及消息持久化
     */
    @Test
    public void testDurable() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        // 声明队列持久化
        channel.queueDeclare("durableQueue",true,false,false,null);
        // 声明消息持久化
        channel.basicPublish("","durableQueue", MessageProperties.PERSISTENT_TEXT_PLAIN,"持久化消息".getBytes());
    }
    /**
     * 测试消息过期时间
     */
    @Test
    public void testTTL() throws IOException, TimeoutException {
        Map<String, Object> arguments = new HashMap<String, Object>();
        // 队列内消息十秒过期
        arguments.put("x-message-ttl", 10000);
        // 队列十秒没有消费者访问该队列则自动删除
        arguments.put("x-expires", 20000);
        Channel channel = RabbitmqConfig.getChannel();
        // 声明队列内消息的过期时间
        channel.queueDeclare("ttlQueue",false,false,false,arguments);
        channel.basicPublish("","ttlQueue",null,"10秒后消息过期".getBytes());
        // 设置单个消息过期时间
        AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder().expiration(20000+"");
        channel.basicPublish("","durableQueue",properties.build(),"20秒后消息过期".getBytes());
    }

    /**
     * x-max-length:用于指定队列的长度,如果不指定,可以认为是无限长,例如指定队列的长度是4,当超过4条消息,前面的消息将被删除,给后面的消息腾位,类似于栈的结构,
     * 当设置了x-max-priority后,优先级高的排在前面,所以基本上排除的话就是排除优先级高的这些
     * x-max-length-bytes: 用于指定队列存储消息的占用空间大小,当达到最大值是会删除之前的数据腾出空间
     * x-max-priority: 设置消息的优先级,优先级值越大,越被提前消费。
     */
    @Test
    public void testMax() throws IOException, TimeoutException {
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-max-length", 4);
        arguments.put("x-max-length-bytes", 1024);
        arguments.put("x-max-priority", 5);
        Channel channel = RabbitmqConfig.getChannel();
        declareDead(arguments,channel);
        channel.queueDeclare("maxQueue",false,false,false,arguments);
        for (int i=1; i<=6;++i){
            AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder().priority(i);
            channel.basicPublish("","maxQueue",properties.build(),("第"+i+"条消息,优先级是:"+i).getBytes());
        }
    }

    /**
     * 声明Dead-exchange、dead-queue
     */
    public void declareDead(Map<String, Object> arguments,Channel channel) throws IOException {
        channel.exchangeDeclare("EXCHANGE_DEAD",BuiltinExchangeType.DIRECT);
        channel.queueDeclare("QUEUE_DEAD",false,false,false,null);
        // 若不指定exchange、queue,则默认使用AMQP default默认exchange,默认使用queue的名字作为routingkey
        channel.queueBind("QUEUE_DEAD","EXCHANGE_DEAD","routing_dead");
        arguments.put("x-dead-letter-exchange", "EXCHANGE_DEAD");
        arguments.put("x-dead-letter-routing-key", "routing_dead");
    }

    /**
     * 测试排它性exclusive
     */
    public static void main(String[] args) {
        Connection connection = null;
        try {
            connection = RabbitmqConfig.getConnection();
            Channel channel = connection.createChannel();
            // 声明一个排它queue
            channel.queueDeclare("testExclusive",false,true,false,null);
            // 关闭当前channel,以证明queue的排它性和channel没关系
            channel.close();
            testExclusiveA(connection);
            testExclusiveB(connection);
            // 在一个服务项目中是没有办法测试排它性的,因为本项目中所有对rabbitmq的连接,在Rabbitmq看来都是一个Connection,所以是不会触发排它锁的,如果需要测试,可以再创建一个项目进行测试
            testExclusiveC();
            // 等待十秒,这区间可以看一下Rabbitmq ui界面,此时rabbitmq ui是可以看到此queue的
            Thread.sleep(10000);
            // 关闭连接,关闭连接后,testExclusive这个queue也会随之删除
            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试发送消息
     * @param connection
     * @throws IOException
     * @throws TimeoutException
     */
    public static void testExclusiveA(Connection connection) throws IOException, TimeoutException {
        Channel channel = connection.createChannel();
        channel.basicPublish("","testExclusive",null,"hello testExclusiveA".getBytes());
        System.out.println("testExclusiveA");
        channel.close();
    }

    /**
     * 测试发送消息
     * @param connection
     * @throws IOException
     * @throws TimeoutException
     */
    public static void testExclusiveB(Connection connection) throws IOException, TimeoutException {
        Channel channel = connection.createChannel();
        channel.basicPublish("","testExclusive",null,"hello testExclusiveB".getBytes());
        System.out.println("testExclusiveB");
        channel.close();
    }

    /**
     * 这里重新建了一个ConnectionFactory,并且获取一个新的Connection,但是事实证明,也是可以发送消息到testExclusive的,因为在同一个项目中建立的连接,
     * 在Rabbitmq看来是一个Connection
     * @throws IOException
     * @throws TimeoutException
     */
    public static void testExclusiveC() throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        channel.basicPublish("","testExclusive",null,"hello testExclusiveC".getBytes());
        System.out.println("testExclusiveC");
        channel.close();
        connection.close();
    }
}

4.Consumer

/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 **/
public class Consumer {
    public static void main(String[] args) {
        try {
            Channel channel = RabbitmqConfig.getChannel();
            /**
             * 这里解释一下为什么需要在订阅queue之前,提前queueDeclare一下,这个是为了防止provider还没有启动,而consumer先启动了,
             * 如果不提前声明的话,那么在rabbitmq中是不存在hello的,那么是没办法订阅消息的,反馈到程序中就是报错!
             * 但是声明时,也要特别注意参数不要弄错
             */
            channel.queueDeclare("hello",false,false,false,null);
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume("hello",true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));
                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

二、Work queues

工作队列模式,是为了应对工作中的微服务架构,或者是多个节点同时消费一个队列的情形,可以看一下下图
Rabbitmq消息中间件初步学习——第二节七种模型分析_第3张图片
C1、C2同时订阅了queue,那么当P发送一条Message到queue中后,这条Message是如何被消费的,这个就是Work queues需要解决的问题!在Work queues模式中,默认是循环消费的模式,不会使一条消费被重复消费,也就是说当多条Message过来后,queue会循环将message发送到C1、C2中,这里其实还涉及到了一个消息确认(ACK)的概念。

官网讲解了关于Work queues的使用场景和将要解决的问题,包括ACK、WorkQueues以及各种情况下所遇到的问题

下面是一个简单的例子

1.POM坐标

pom坐标依然和上边一致

2.工具类

工具类依然和上面一致

3.Producer

/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 * @create: 2021-01-04 21:47
 **/
public class Producer {

    @Test
    public void sendManyM() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        // 声明work queue ,并将它设置为持久化
        channel.queueDeclare("work-queues",true,false,false,null);
        for (int i = 0;i<10;++i){
            String message = "第"+i+"条消息";
            channel.basicPublish("","work-queues", MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("utf-8"));
        }
        channel.close();
        channel.getConnection().close();
    }
}

4.Consumer

/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-04 21:47
 **/
public class ConsumerC1 {
    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 这里解释一下为什么需要在订阅queue之前,提前queueDeclare一下,这个是为了防止provider还没有启动,而consumer先启动了,
             * 如果不提前声明的话,那么在rabbitmq中是不存在hello的,那么是没办法订阅消息的,反馈到程序中就是报错!
             * 但是声明时,也要特别注意参数不要弄错
             */
            channel.queueDeclare("work-queues",true,false,false,null);
            /**
             * 设置一次性接受信息的最大数量
             * 如果不设置,默认不受限制,那么Rabbitmq会一致发送消息至consumer这里,等待consumer处理
             * 这个其实是因为channel是异步处理消息的,哪怕下面设置了手动确认消息,消息依然会发送给consumer,等待处理确认
             * 设置了最大的接受消息数量后,在未确认消息之前,最多可以给consumer发送最大接受数量的消息,否则只能等待确认消息后才能继续发送
             */
            channel.basicQos(1);
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume("work-queues",false,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    try {
                        Thread.sleep(1000);
                        System.out.println("Receive==="+new String(body));
                        /**
                         * 手动确认消息
                         *  envelope.getDeliveryTag():delivery Tag,消息标识
                         *  multiple:批量处理消息
                         */
                        channel.basicAck(envelope.getDeliveryTag(),false);
                        /**
                         * 手动发送不确定消息
                         * envelope.getDeliveryTag():delivery Tag,消息标识
                         * multiple:批量处理消息
                         * requeue:重新排列,true:重新发送消息;false:丢弃或者死信队列
                         */
//                        channel.basicNack(envelope.getDeliveryTag(),false,true);
                        /**
                         * 拒绝消息
                         * envelope.getDeliveryTag():delivery Tag,消息标识
                         * requeue:重新排列,true:重新发送消息;false:丢弃或者死信队列
                         */
                        channel.basicReject(envelope.getDeliveryTag(),false);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-04 21:48
 **/
public class ConsumerC2 {
    public static void main(String[] args) {
        try {
            Channel channel = RabbitmqConfig.getChannel();
            /**
             * 这里解释一下为什么需要在订阅queue之前,提前queueDeclare一下,这个是为了防止provider还没有启动,而consumer先启动了,
             * 如果不提前声明的话,那么在rabbitmq中是不存在hello的,那么是没办法订阅消息的,反馈到程序中就是报错!
             * 但是声明时,也要特别注意参数不要弄错
             */
            channel.queueDeclare("work-queues",true,false,false,null);
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume("work-queues",true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));
                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

    }
}

这里有两个consumer来体现work queues工作模式的循环发送消息的特点,不过这里我使用了手动确认消息,来模拟当一个consumer1因为程序出现了延迟情况,Rabbitmq将消息优先发送给consumer2中,而不必全部发送给consumer1,造成更多的延迟

其实我认为我写的已经很详细了,但是和官网文档比起来还是差了太多,英语能力还可以的,或者想要了解更多的,可以直接移步官网 Work queues 文档,也希望可以在我的博客下讨论英语学习!

三、Publish/Subscribe

在这个模式中,出现了交换器,之前两个模式是使用默认交换机的,首先说一下这个模式是为了解决什么问题吧。
在Hello World中,我们了解到如何将一条消息发送到Rabbitmq Server,并由Rabbitmq Server转发到Consumer中;
在Work queues中,我们知道如何将一条消息发送到queue后,多个Consumer如何合理分配这条消息,保证不会重复消费!
那么在Public/Subscribe中,我们希望将一条消息发送给多个Consumer,也就是说我们有一条消息,这条消息应该给多个Consumer去处理!
这个在实际的业务场景中,用到的地方也是非常多的,比如:
在一个电商平台,用户在购物车中下单购买了一个商品,那么这个订单的流转过程中都经过了哪些步骤?比如它肯定需要发送一个消息给购物车服务,给用户当前购物车数量减一;它需要给商家服务发送一条消息,商家服务需要多一条订单任务;需要给财务系统,财务需要多个统计…等等
这也就是一条消息需要被多个Consumer消费的问题!
那么如何解决它呢?这肯定不能再Producer中,直接给多个系统发送消息,这也是可以的,只是太笨拙!这个就是Public/Subscribe需要解决的问题,

在模式中可以了解到Exchange的概念,Exchange在前面的内容中也有接触,它是AMQP协议中的一个重要概念,说简单点就是路由消息的,将对应消息根据Exchange的类型不同路由到不同的queues中!那么Exchange都有什么类型呢?
Rabbitmq消息中间件初步学习——第二节七种模型分析_第4张图片
从上图可以看出来,Exchange主要的类型是direct、fanout、headers、topic,其中,在实际工作中,headers用的还是比较少的!
Publish/Subscribe模式中主要讲解fanout类型,其实其他类型可以看成是fanout的一个特殊化后的类型,这个让我想到了一个很形象的例子

比如:
一个四边形,当对边相等并平行,那它就是长方形;
当一个长方形,四边相等并平行,那它就是一个菱形;
当一个菱形,四个角都是直角,那它就是一个正方形。

这个例子主要是为了阐述一种子集的关系!
下面看实战例子来详细了解吧!

1.Producer

/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 * @create: 2021-01-05 22:23
 **/
public class Producer {

    private final String EXCHANGE = "MESSAGE_FANOUT";
    private final String MESSAGE = "广播消息";

    @Test
    public void sendMessage() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        /**
         * 声明一个Exchange
         * exchange:名称
         * type:类型
         */
        channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);
        /**
         * 发送消息
         * exchange:交换机名称
         * routingKey:因为Exchange类型为fanout,所以routingkey没有意义,因此为空
         */
        channel.basicPublish(EXCHANGE,"", null,MESSAGE.getBytes("utf-8"));
        channel.close();
        channel.getConnection().close();
    }
}

2. Consumer


/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-05 22:23
 **/
public class ConsumerC1 {
    private final static String EXCHANGE = "MESSAGE_FANOUT";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:路由key,因为exchange type是fanout,所以routingKey为空
             */
            channel.queueBind(queueName,EXCHANGE,"");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-05 22:23
 **/
public class ConsumerC2 {
    private final static String EXCHANGE = "MESSAGE_FANOUT";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:路由key,因为exchange type是fanout,所以routingKey为空
             */
            channel.queueBind(queueName,EXCHANGE,"");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

这两个Consumer代码基本一致,也是很简单的实现了广播的效果

四、Routing

在官方文档中,Routing模式其实就是Exchange的Direct类型,其实这个类型就是Fanout类型的子集,对Fanout类型进行了一下Filter就可以得到Direct类型了!在Direct类型中有一个概念就是RoutingKey,Exchange是通过RoutingKey来将Message发送到指定的queue的!这个就和Fanout无脑的广播有明显的区别了!

具体怎么实现的呢,我引入一张官方的解释图
Rabbitmq消息中间件初步学习——第二节七种模型分析_第5张图片
简单解释一下,Exchange的类型是Direct,将两个Queue绑定到这个Exchange上,绑定时分别定义不同的RoutingKey,可以看到上面!
可以想一下上面的模型中,当我们给RoutingKey为info的发送数据时,消息是发给C2的,当我们给RoutingKey为error的发送数据时,消息是发给C1、C2的,当他们绑定的RoutingKey相同时,也就类似于广播的效果了

下面看一下代码示例吧

1.Producer


/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 * @create: 2021-01-05 22:53
 **/
public class Producer {

    private final String EXCHANGE = "MESSAGE_DIRECT";

    @Test
    public void sendMessage() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        /**
         * 声明一个Exchange
         * exchange:名称
         * type:类型
         */
        channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT);
        /**
         * 发送消息
         * exchange:交换机名称
         * routingKey:DIRECT类型,必须需要routingKey
         */
        channel.basicPublish(EXCHANGE,"error", null,"报错信息".getBytes("utf-8"));
        channel.basicPublish(EXCHANGE,"info", null,"提示信息".getBytes("utf-8"));
        channel.basicPublish(EXCHANGE,"warm", null,"警告信息".getBytes("utf-8"));
        channel.close();
        channel.getConnection().close();
    }

}

2. Consumer


/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-05 22:55
 **/
public class ConsumerC1 {
    private final static String EXCHANGE = "MESSAGE_DIRECT";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:DIRECT类型,必须需要routingKey
             */
            channel.queueBind(queueName,EXCHANGE,"error");
            channel.queueBind(queueName,EXCHANGE,"info");
            channel.queueBind(queueName,EXCHANGE,"warm");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-05 22:55
 **/
public class ConsumerC2 {
    private final static String EXCHANGE = "MESSAGE_DIRECT";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:DIRECT类型,必须需要routingKey
             */
            channel.queueBind(queueName,EXCHANGE,"error");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

代码差距不大,这种案例都是很简单的,只是为了熟悉Rabbitmq的特性罢了

五、Topics

Topics模式就是Exchange中的Topic类型,Topic类型和Direct有什么区别呢?
这个其实很好分辨,简单的说就是多了两个通配符,*和#,这个两个通配符

*:代表一个单词
#:代表零个或者多个单词

当然,Topic的RoutingKey是由一组单词组成的,这样才可以完成路由匹配的规则,具体是什么样的,举个例子吧!
Rabbitmq消息中间件初步学习——第二节七种模型分析_第6张图片
一看是拿出官网的例子,如果发送file.orange.animal的routingKey的话,那么消息肯定是发送到Q1中的;如果发送lazy的routingKey的话,那么消息肯定是发送到Q2中的,如果发送lazy.orange.animal呢?可以根据上边提到的通配符匹配的原则,这样的消息是可以发送到两个queue的,因为它同时匹配两种情况!
从这里也可以看出来,当我们Exchange 的类型是Topic的时候,我们不使用通配符的情况下,那么Topic就类似于Direct,当我们把RoutingKey设置成#,那么Topic就类似于Fanout。
下面让看这个例子,来加深一下刚刚说的

1.Producer


/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 * @create: 2021-01-06 00:10
 **/
public class Producer {

    private final String EXCHANGE = "MESSAGE_TOPIC";

    @Test
    public void sendMessage() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        /**
         * 声明一个Exchange
         * exchange:名称
         * type:类型
         */
        channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC);
        /**
         * 发送消息
         * exchange:交换机名称
         * routingKey:DIRECT类型,必须需要routingKey
         */
        channel.basicPublish(EXCHANGE,"log.error", null,"报错信息".getBytes("utf-8"));
        channel.basicPublish(EXCHANGE,"log.info", null,"提示信息".getBytes("utf-8"));
        channel.basicPublish(EXCHANGE,"log.warm", null,"警告信息".getBytes("utf-8"));
        channel.close();
        channel.getConnection().close();
    }
}

2.Consumer

/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-06 00:15
 **/
public class ConsumerC1 {
    private final static String EXCHANGE = "MESSAGE_TOPIC";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:TOPIC类型,相较于其他几个类型,多了通配符的概念
             * 也就是
             *  *:代替一个letter
             *  #:代替零个或者多个letter
             *  其他基本一致
             */
            channel.queueBind(queueName,EXCHANGE,"log.*");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息
 * @create: 2021-01-06 00:15
 **/
public class ConsumerC2 {
    private final static String EXCHANGE = "MESSAGE_TOPIC";

    public static void main(String[] args) {
        try {
            final Channel channel = RabbitmqConfig.getChannel();
            /**
             * 声明一个Exchange
             * exchange:名称
             * type:类型
             */
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.TOPIC);
            /**
             * 随机声明一个队列,并获取对应的queue的名称
             * 默认特性:
             * autoDelete:true
             * exclusive:true
             */
            String queueName = channel.queueDeclare().getQueue();
            /**
             * 绑定queue到exchange
             * queue:队列名称
             * exchange:交换机名称
             * routingKey:TOPIC类型,相较于其他几个类型,多了通配符的概念
             * 也就是
             *  *:代替一个letter
             *  #:代替零个或者多个letter
             *  其他基本一致
             */
            channel.queueBind(queueName,EXCHANGE,"log.error");
            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueName,true,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));

                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

目前来讲,Exchange四大类型中,已经介绍了三种类型了,就剩下了Headers,这个如果在接下来两种模型中没有介绍到的话,那么再单独介绍一下!

六、RPC

在研究Rabbitmq的RPC模型之前,因为对于RPC协议的不了解,所以不得不拿出一天的时候去深入理解一下什么是RPC。

关于上面这点网上的解释也有很多,但是…嗯,确实参差不齐,如果不多阅读一些文章的话,确实容易理解错误,因为好多博客主其实都是复制粘贴的,当然我感觉这并不可耻,因为没有时间整理,还想留住好的内容,这样记录一下也正常!这事我也做过,不过一般都是设置成私人可见而已。当然我认为了解一门技术的最好方法并不是读一篇相关文章就可以称之为对一门技术有一个直观的认识,不过这篇文章是谁写的,难免有些不足,最好多阅读一些文章,整合理解!

就不啰嗦了,下面将先按照我的理解介绍一下RPC,如有不足,请评论指点!

1 从协议层面看RPC和HTTP

首先我比较认可这个segmentfault的答案,RPC和HTTP并不是一个层面的概念,论其可比性,比较不好描述!

先说一下HTTP吧,然后再说RPC,Http这个概念,正在从事网络编程或者学习这方面编程知识的人应该都是不陌生的,它是一个应用层上的网络传输协议——超文本传输协议,Http协议虽然本身没有规定底层基于什么传输协议,但是目前来说基本上是基于TCP协议。

看一下百科对于HTTP1.1的解释

它是用来在Internet上传送超文本的传送协议。它是运行在TCP/IP协议簇之上的HTTP应用协议,它可以使浏览器更加高效,使网络传输减少。任何服务器除了包括HTML文件以外,还有一个HTTP驻留程序,用于响应用用户请求。您的浏览器是HTTP客户,向服务器发送请求,当浏览器中输入了一个开始文件或点击了一个超级链接时,浏览器就向服务器发送了HTTP请求,此请求被送往由URL指定的IP地址。驻留程序接收到请求,在进行必要的操作后回送所要求的文件。

再了解一下HTTP2.0

HTTP/2 (原名HTTP/2.0)即超文本传输协议 2.0,是下一代HTTP协议。是由互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis (httpbis)工作小组进行开发。是自1999年http1.1发布后的首个更新。HTTP 2.0在2013年8月进行首次合作共事性测试。在开放互联网上HTTP 2.0将只用于https://网址,而 http://网址将继续使用HTTP/1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。DANE RFC6698允许域名管理员不通过第三方CA自行发行证书。

这块太多了,就简单引入一些介绍吧,具体可以去百科看一下!这里再看一下他们之间的区别,这里也是简单找了一个博客,其实HTTP不难理解,就是在Internet上,多个客户端互相调用的协议,1.1、2.0也不过是更新的版本,越高版本理论上对于最新技术支持越好。

下面再看一下RPC:
首先需要提到的是RPC也是一种协议,它本身通信可以基于TCP、也可以基于HTTP2.0(比如GRPC),看一下百科的解释

RPC是远程过程调用(Remote Procedure Call)的缩写形式。SAP系统RPC调用的原理其实很简单,有一些类似于三层构架的C/S系统,第三方的客户程序通过接口调用SAP内部的标准或自定义函数,获得函数返回的数据进行处理后显示或打印。

看到这个是不是觉得RPC和HTTP很像,都是调用返回结果,这个也是网络上很多标题如“RPC和HTTP的区别”的文章的由来,因为在程序员来看,它们确实很像,这个就不得不感叹像我一样的只知道编程语言,不曾深入了解计算机基础的悲哀了!
为什么觉得它们很像呢?那是因为对比的是它们在程序中使用的调用方式,也就是服务调用,这个等下讲。如果只是说它们本身——RPC协议、HTTP协议,那么他们就不是一个层面的概念,因此也没有可比性。

HTTP协议其实可以看成是RPC协议的子集,或者说HTTP协议本身也可以看成是一种简陋的RPC协议更好一些,RPC可以从字面上理解,它侧重于远程过程调用,是不关心实现细节的,也就是说实现了远程过程调用这种模式(隐藏通讯细节),都可以称之为RPC的实现;HTTP是一个有规范描述的通信协议,RPC可以以HTTP作为通信载体,也可以使用自定义的TCP协议作为通信载体,HTTP和TCP都可以看成是底层RPC的通信实现方式!

总结一下

  1. http、tcp,甚至本篇主要介绍的mq都是传输通信协议;而RPC是远程服务调用方式的一种协议规范。
  2. http、tcp、mq都可以看成是RPC底层通信实现方式
  3. rpc和http没有对比性

2 从服务调用看RPC和HTTP

我认为在这个角度看RPC和HTTP才是网络上很多提到的区别的由来,不过这个可比性主要是对于远程调用这个过程,是否进行了封装。若是封装,那么就可以称之为RPC服务调用,若是没有封装,需要设置调用参数等,那么就是HTTP服务调用(常用)。

这里引入一点额外的介绍

rpc中,jsonrpc、xmlrpc一般认为是使用了json、xml序列化方式,使用http作为传输协议;
grpc,序列化方式是 protobuf,传输协议是http2.0

看了上面的介绍,再来几篇文章里边说的rpc和http的区别,如:RPC与Http的区别、RPC和HTTP主要的区别、RPC(Remote Procedure Call Protocol)——远程过程调用协议等等,这些指的都是服务调用中它们的区别,这个在RPC与Http的区别体现的很好,有一句话,我也认为很正确

RPC强调的是过程调用,调用的过程对用户而言是应该是透明的,用户不应该关心调用的细节,可以像调用本地服务一样调用远程服务。所以RPC一定要对调用的过程进行封装

那么在微服务中,有注册中心,使用feign进行远程调用,那么feign这种调用方式算是RPC调用还是HTTP调用呢?
这种调用方式对用户来说,肯定是透明的,因为我们只需要在指定服务名和相对路径就可以了。其他的就和调用本地方法一样,进行调用就可以了。我认为首先这肯定是HTTP调用方式(非其他自定义TCP传输协议),而且也是RPC调用。
尤其是在我们的项目中体现的更明显,在我们现在的项目中,有个core核心jar包,来定义一些接口,然后同时被Client和Server引入,Client通过接口去调用Server,Server实现具体的逻辑!接口是以feign技术的。这种调用细节的封装,应该可以算做是RPC调用的吧(当然肯定是没有如WebService:CXF这种封装的那么彻底)。

这里提一下,在我看来目前SpringCloud服务治理中,真正属于HTTP调用的,应该算是Httpclient,直接定义调用参数(如url、UserAgent等),去直接调用其他服务对外暴露的post、get接口,这种应该不属于RPC调用,只属于HTTP调用!

当然这个划分只属于我总结后的一些想法,主要这里体现到RPC调用中对于封装程度的一个体现,如果要封装的非常彻底,如CXF这种,那么微服务中feign就不能算是RPC调用,这块都说dubbo是Rpc调用方式,因为我没有接触过dubbo,所以不太清楚dubbo对于接口调用的封装是什么样的?这里也留下疑问,希望哪位了解dubbo的大佬,可以参与讨论一下!

其实这个内容应该算是一个新的博客,但是这里首先我自己的理解不敢说一定正确(没有细致的学习RPC协议,只是粗略的理解协议的概念),所以就放到这里了!有了解的希望对这里多加讨论

3 Rabbitmq中的RPC

上面我认为是对RPC有了一个较清楚的了解了,那么Rabbitmq中RPC模型是什么意思的呢?
还记得在上面中,有提到RPC可以基于TCP、HTTP、MQ这些传输协议来实现,而Rabbitmq中RPC模型就是以Rabbitmq作为载体进行实现的方式,这里就没啥好解释的了,下面直接看一下如何通过Rabbitmq来实现远程调用(RPC)吧

这里直接使用了官网的案例了

3.1 客户端


/**
 * @title: RPCClient
 * @description: 关于RPC的解释应该在博客中解释的很清楚了,这里就不赘述了
 *                下面主要是使用基于Rabbitmq实现RPC请求的过程,这个类是Client,发送RPC请求并且得到RPC的一个请求结果
 * @author heshuai
 * @date 2021年01月19日 21:11
 */
public class RPCClient {

    private static Channel channel;
    private static String requestQueueName = "rpc_queue";

    public static void main(String[] argv) throws IOException {
        try {
            // 新建一个channel
            channel = RabbitmqConfig.getChannel();
            // 循环调用RPCServer,查看返回结果
            for (int i = 0; i < 16; i++) {
                String i_str = Integer.toString(i);
                System.out.println(" [x] Requesting fib(" + i_str + ")");
                String response = call(i_str);
                System.out.println(" [.] Got '" + response + "'");
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 关闭connection
            channel.getConnection().close();
        }
    }

    /**
     * RPC请求实际执行方法
     * @param message
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static String call(String message) throws IOException, InterruptedException {
        // 生成唯一的correlationId,用来辨别返回response
        final String corrId = UUID.randomUUID().toString();
        // 生成返回queue,也就是当RPC request到达server后,由server返回的response所发送的queue
        String replyQueueName = channel.queueDeclare().getQueue();
        /**
         * 定义每条消息的配置信息,主要配置其中的replyTo,correlationId
         * replyTo:response返回queue
         * correlationId:辨别queue中的response是否符合当前的request
         */
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();
        // 发送request请求
        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
        // 创建一个阻塞queue,数量为1
        final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
        /**
         * 监听response返回queue
         * queue: queue名字
         * autoAck:是否自动确认消息
         * deliverCallback:发送消息时的回调
         * cancelCallback:取消回调通知
         * return: 返回consumerTag,唯一标识consumer在queue中的状态
         */
        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            // 接受等于当前correlationId的Response,若不等于则丢弃
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                // 插入一个元素
                response.offer(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });
        // 获取一个元素,若当前无元素,则block当前线程,直到可以或许元素为止;类似于Future方法
        String result = response.take();
        // 取消回调通知
        channel.basicCancel(ctag);
        return result;
    }

}

3.2 服务端


/**
 * @author heshuai
 * @title: RPCServer
 * @description:  RPC服务端,基于Rabbitmq(AMQP)实现RPC系统,这里接受到一个数字参数并返回当前数字所对应Fibonacci序列位置的值
 * @date 2021年01月19日 22:35
 */
public class RPCServer {
    // 监听queue名字
    private static final String RPC_QUEUE_NAME = "rpc_queue";
    private static Channel channel;

    /**
     * 返回一个Fibonacci数字
     * @param n 处于Fibonacci序列中的位置
     * @return 返回Fibonacci数值
     */
    private static int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] argv) throws Exception {

        try {
            channel = RabbitmqConfig.getChannel();
            /**
             * 声明queue,其他参数就不介绍了,其他模型中介绍太多次了
             */
            channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
            /**
             * 清除指定queue的内容,就是将queue中消息清空
             */
            channel.queuePurge(RPC_QUEUE_NAME);
            /**
             * 设置一次性预处理消息数量
             */
            channel.basicQos(1);

            System.out.println(" [x] Awaiting RPC requests");

            Object monitor = new Object();

            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                /**
                 * 响应Message的配置信息
                 * correlationId:与Request相关联的唯一Id
                  */
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();
                /**
                 * RPC响应结果
                 */
                String response = "";

                try {
                    // Request参数
                    String message = new String(delivery.getBody(), "UTF-8");
                    int n = Integer.parseInt(message);

                    System.out.println(" [.] fib(" + message + ")");
                    // 获取Fibonacci数字,并封装结果
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e.toString());
                } finally {
                    // 返回Response给Client,采用默认Exchange
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                    // 手动确认单条消息
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };

            channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
            // 使main线程不关闭,等待consume消息
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 关闭连接
            channel.getConnection().close();
        }
    }
}

相关代码所对应的含义已经在代码中有特别的注释了,就不额外解释了
说一下使用Rabbitmq搭建RPC的优点吧(官网解释,我只是搬运工)

  • 若是觉得Server服务端请求过慢,可以搭建多个Server服务来实现分布式服务减轻服务压力,这里就用到了上边的Work queues模式了;
  • 因为RPC请求是异步进行的,所以只需要一个RPC请求往返开销就可以了

说一下官网中的免责声明:
本篇所有的样例只是学习所用,案例都比较简单,用于生产肯定是远远不够的,还需要考虑实际业务情况来进行优化(肯定个人理解总结了一下)
Rabbitmq消息中间件初步学习——第二节七种模型分析_第7张图片

七、Publisher Confirms

先字面理解一个,就是发布确认,在之前的模型中也有提到消息确认的问题,但是这个消息确认和这个模型不太一样,以前提到的消息确认是指Consumer和Rabbitmq服务之间进行的一次消息确认。而这个模型中的发布确认是指Provider和Rabbitmq之间进行的消息确认。
即:Provider(Publisher)需要确认消息是否已经被Rabbitmq接受

前面不知道是否有提到这个模型,如果提到了,可能理解错了,这个是Publisher Confirms 模型,是发布者和rabbitmq 服务端之间的一个消息安全机制,不涉及消息接受者。

这个模型是为了Provider可以确保消息是否已经到达Rabbitmq,可以根据回调的ack、nack-ed来处理消息,比如:消息遗失(nack-ed)未到达Rabbitmq,那么可以重新发布消息来实现消息不丢失的确保机制。
但是这里需要注意
Rabbitmq消息中间件初步学习——第二节七种模型分析_第8张图片
这个是Rabbitmq的客户端代码中的确认回调接口,这里提到一个问题,Rabbitmq返回了一个nack-ed,但是可能消息已经到达Rabbitmq并且成功递送给了Consumer,这个Rabbitmq 是不能绝对保障的。

这里如果为了绝对实现消息的安全性(排除硬件情况),确保消息唯一递送给了Consumer,那么这里需要考虑到很多方面,比如除了开启Publisher Confirm,实现失败消息重新发布外,还需要在Consumer实现手动消息确认(ack)并且判断当前消息是否已经被消费过,若消费过则不可以重复消费。
需要多方面考虑一下。比如,Rabbitmq宕机,实现队列、消息持久化等

下面看一下例子吧,这个例子来自官网,我做了必要的解释

/**
 * @author heshuai
 * @title: PublisherConfirms
 * @description: Rabbitmq 官方实现Publisher Confirm(发布确认)实现,此模式为了提供了消息发布者可以确定消息到达Mq服务,如果再结合Server手动消息
 *               确认,那么就可以最大程度保证消息的不丢失,以下内容是Rabbitmq的实现方式,我只是对一下内容做必要的注释理解
 *               本案例比较简单,如果用于生产还需优化!!!
 * @date 2021年01月28日 19:44
 */
public class PublisherConfirms {
    // 官方测试50000条消息,太多了,这次测试1000
    static final int MESSAGE_COUNT = 1_000;

    public static void main(String[] args) throws Exception {
        publishMessagesIndividually();
        publishMessagesInBatch();
        handlePublishConfirmsAsynchronously();
    }

    /**
     * 单个消息同步确认实现发布者确认
     * 优点:实现简单
     * 缺点:发布消息受到影响,一秒内最多几百个发布
     * @throws Exception
     */
    static void publishMessagesIndividually() throws Exception {
        try (Connection connection = RabbitmqConfig.getConnection()) {
            Channel ch = connection.createChannel();

            String queue = UUID.randomUUID().toString();
            // 声明一个队列
            ch.queueDeclare(queue, false, false, true, null);
            // 激活当前channel的发布者确认机制, 默认不启动
            ch.confirmSelect();
            // 开始时间戳
            long start = System.nanoTime();
            // 循环发布若干条消息进行测试
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String body = String.valueOf(i);
                // 发布消息,采用默认Exchange
                ch.basicPublish("", queue, null, body.getBytes());

                /**
                 * 同步等待发布确认返回,可设置超时时长,时间单位是TimeUnit.MILLISECONDS
                 * 超时则抛出异常:TimeoutException
                 * nack-ed则抛出异常:IOException
                 */
                ch.waitForConfirmsOrDie(5_000);
            }
            // 结束时间戳
            long end = System.nanoTime();
            System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }

    /**
     * 同步批量发布确认
     * 优点:发布数量大大提升,时间也较简单
     * 缺点:当出现nack-ed回复时,无法确认一批消息中哪条消息是nack-ed,所以若需要重新发布消息,则需要将一批所有消息进行重复发送
     * @throws Exception
     */
    static void publishMessagesInBatch() throws Exception {
        try (Connection connection = RabbitmqConfig.getConnection()) {
            Channel ch = connection.createChannel();

            String queue = UUID.randomUUID().toString();
            ch.queueDeclare(queue, false, false, true, null);

            ch.confirmSelect();
            // 100消息为一组消息进行发送
            int batchSize = 100;
            // 当前发送消息的数量
            int outstandingMessageCount = 0;

            long start = System.nanoTime();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String body = String.valueOf(i);
                ch.basicPublish("", queue, null, body.getBytes());
                outstandingMessageCount++;
                // 若整组消息发送完成后,同步等待发布确认,若这里出现了nack-ed,则可能无法确认是具体哪条消息出现了nack-ed
                if (outstandingMessageCount == batchSize) {
                    ch.waitForConfirmsOrDie(5_000);
                    outstandingMessageCount = 0;
                }
            }
            // 防止不满一组消息,在之前没有等待发布确认,这里处理一下
            if (outstandingMessageCount > 0) {
                ch.waitForConfirmsOrDie(5_000);
            }
            long end = System.nanoTime();
            System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }

    /**
     * 异步处理发布确认
     * 优点:灵活处理每一条消息发布确认结果,因为是异步,所以性能较好
     * 缺点:实现较复杂,涉及到复杂的场景时需要考虑的方面较多
     * @throws Exception
     */
    static void handlePublishConfirmsAsynchronously() throws Exception {
        try (Connection connection = RabbitmqConfig.getConnection()) {
            Channel ch = connection.createChannel();

            String queue = UUID.randomUUID().toString();
            ch.queueDeclare(queue, false, false, true, null);

            ch.confirmSelect();
            /**
             * 并发环境下map集合,可序列化排序key
             */
            ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
            /**
             * 发布确认异步回调方法
             */
            ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
                /**
                 *  sequenceNumber:序列数字,关联消息和发布确认之间的
                 *  multiple: false:一条消息被ack/nack; true:所有低于等于当前sequenceNumber的消息被ack/nack
                 */
                if (multiple) {
                    // 为true,则查看集合outstandingConfirms中所有低于或等于当前sequenceNumber的消息,并将它从集合中清除
                    ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
                            sequenceNumber, true
                    );
                    confirmed.clear();
                } else {
                    // 移除单条消息
                    outstandingConfirms.remove(sequenceNumber);
                }
            };
            /**
             * 在channel上添加一个发布确认的监听器
             * 分为ack处理和nack-ed处理
             * 第一个为ack处理回调,第二个为nack-ed处理回调
             */
            ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
                // nack-ed 回调处理逻辑

                // 在集合中获得nack-ed消息
                String body = outstandingConfirms.get(sequenceNumber);
                // 记录当前nack-ed消息日志
                System.err.format(
                        "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
                        body, sequenceNumber, multiple
                );
                // 在集合中移除对应消息
                cleanOutstandingConfirms.handle(sequenceNumber, multiple);
            });

            long start = System.nanoTime();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String body = String.valueOf(i);
                // 将对应消息和发布消息唯一标识关联起来并且存储在集合中
                outstandingConfirms.put(ch.getNextPublishSeqNo(), body);
                ch.basicPublish("", queue, null, body.getBytes());
            }
            // 超过60秒未全部返回确认消息,则报错
            if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
                throw new IllegalStateException("All messages could not be confirmed in 60 seconds");
            }

            long end = System.nanoTime();
            System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }

    /**
     * 等待若干时间
     * @param timeout 持续等待市场
     * @param condition 额外判断条件
     * @return
     * @throws InterruptedException
     */
    static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException {
        int waited = 0;
        // 每一百毫秒查看一下判断条件是否成立,若成立则返回;最后持续timeout所包含的持续时长
        while (!condition.getAsBoolean() && waited < timeout.toMillis()) {
            Thread.sleep(100L);
            waited = +100;
        }
        return condition.getAsBoolean();
    }

}

这里就没有Consumer 了,因为测试这个模型不需要Consumer 的参与,所以我也就不写了。

中途啰嗦一下
这几个例子写到这里写了快四万字了,而且历时大概一个月,主要也是最近工作比较忙,只能断断续续去看文档写例子,确实有点慢,而且在写最后一个模型的时候,我发现了我一个很大的问题,那就是java基础确实比较薄弱,关于各种集合在实际工作中用的还是比较少,都快忘了。比如这个ConcurrentNavigableMap集合,还是阅读了相关的代码才明白大概意思,因为使用不多也就没在代码中过多解释注释,如果想要学习,可以看一下相关文档。
这一问题,最近也在同步阅读java编程思想这本书,希望能够解决。
到时候也会将这本书的相关内容总结一下写成博客分享出来,就是不知道需要多久,共勉吧

最后再补充一下Exchange类型中的Header类型吧。

八、Exchange Header

Exchange 的Header类型和其他三种差不多,不过其他的都是在Exchange和Queue之间绑定RoutingKey,发布消息时指定RoutingKey;而这个Header在绑定Exchange和Queue时使用的是binding parameters属性,发布消息时指定消息Header属性配置才可以路由到对应的Queue。
这么说很模糊吧,看一下图
Rabbitmq消息中间件初步学习——第二节七种模型分析_第9张图片
这个是Exchange和Queue 之间的绑定关系,可以看到RoutingKey为空,而Arguments是有值,并且值是不等的,Arguments的值是key/value是不固定的,他只要一个内置的属性,那就是x-match,先说一下x-match,它有两个value值,分别是all(默认值)、any。

  • all:默认值。一个传送消息的header里的键值对和交换机的绑定参数键值对全部匹配,才可以路由到对应交换机
  • any:一个传送消息的header里的键值对和交换机的绑定参数键值对任意一个匹配,就可以路由到对应交换机

注:不管两种哪个模式,不需要传送消息Header属性和交换机绑定参数个数完全一致,只需要满足交换机绑定参数即可,也就是说可以传送消息Header属性可以多于交换机绑定参数个数,只需要传送消息Header属性其中某些属性符合交换机绑定参数的规则就可以了

再举个例子,来理解一下Exchange是如何路由消息的
等下的代码案例,会创建两个consumer,消息接受者,并且绑定Exchange和Queue之间的参数,参数如下

    1. flag=consumer, tag=test
    1. flag=consumer

设两个绑定参数x-match=all,可以看出来,第一个的key/value包含第二个key/value,所以发送的消息header属性符合第一个必然符合第二个queue,反之,符合第二个queue却不可能符合第一个
设两个绑定参数x-match=any,则符合第二个queue必然符合第一个queue,符合第一个queue则不一定符合第二个queue,因为可能发送的消息Header只符合第一个queue的tag参数,这样的话,就不符合第二个queue了

再根据代码综合理解一下吧

1. Producer


/**
 * @program: rabbitmq-demo
 * @description: 消息生产者
 *              如这个案例,创建两个consumer,消息接受者,并且绑定Exchange和Queue之间的参数,参数如下
 *              1. flag=consumer, tag=test
 *              2. flag=consumer
 *              设绑定参数x-match=all,可以看出来,第一个的key/value包含第二个key/value,所以发送的消息header属性符合第一个必然属于第二个queue,
 *              反之,符合第二个queue却不符合第一个
 *              设绑定参数x-match=any,则符合第二个queue必然符合第一个queue,符合第一个queue则不一定符合第二个queue,因为可能发送的消息Header只符合第一个queue的tag参数,这样的话,就不符合
 *              第二个queue了
 * @create: 2021-01-28 22:05
 **/
public class Producer {

    private final String EXCHANGE = "MESSAGE_HEADER";

    @Test
    public void sendMessage() throws IOException, TimeoutException {
        Channel channel = RabbitmqConfig.getChannel();
        /**
         * 声明一个Exchange
         * exchange:名称
         * type:类型
         */
        channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.HEADERS);

        List<Map> headers = new ArrayList<>();
        Map<String,Object> header1 = new HashMap<>();
        header1.put("flag","consumer");
        header1.put("tag","test");
        headers.add(header1);
        Map<String,Object> header2 = new HashMap<>();
        header2.put("flag","consumer");
        headers.add(header2);
        Map<String,Object> header3 = new HashMap<>();
        header3.put("flag","consumer");
        header3.put("tag","test");
        header3.put("other","error");
        headers.add(header3);
        Map<String,Object> header4 = new HashMap<>();
        header4.put("tag","test");
        headers.add(header4);
        Map<String,Object> header5 = new HashMap<>();
        header5.put("flag","#");
        headers.add(header5);
        this.send(channel,headers);

        channel.close();
        channel.getConnection().close();
    }

    /**
     * 封装一个专门发送消息的方法
     * @param channel
     */
    private void send(Channel channel, List<Map> headers) throws IOException {

        Iterator iterator = headers.iterator();
        while (iterator.hasNext()){
            Map<String,Object> header = (Map<String,Object>)iterator.next();
            // 定义消息配置信息,这里主要定义消息Header信息
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().headers(header).build();
            Iterator<String> keys = header.keySet().iterator();
            StringBuffer message = new StringBuffer();
            while (keys.hasNext()){
                String key = keys.next();
                message.append(key+"="+header.get(key)+",");
            }
            /**
             * 发送消息
             * exchange:交换机名称
             * routingKey:Exchange type 是 Header,则routingkey为空
             * props:定义消息配置属性信息,比如 Routing Header
             */
            channel.basicPublish(EXCHANGE,"", properties,message.toString().getBytes());
        }
    }
}

2 Consumer1


/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息, Header all
 **/
public class Consumer_1 {

    private final static String EXCHANGE = "MESSAGE_HEADER";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitmqConfig.getChannel();
            // 声明交换机
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.HEADERS);
            // 声明一个独占、自动删除的queue
            String queueNameAll = channel.queueDeclare().getQueue();
            /**
             * 绑定参数
             * x-match:
             *       all: 默认值。一个传送消息的header里的键值对和交换机的绑定参数键值对全部匹配,才可以路由到对应交换机
             *       any: 一个传送消息的header里的键值对和交换机的绑定参数键值对任意一个匹配,就可以路由到对应交换机
             *       注:不管两种哪个模式,不需要传送消息Header属性和交换机绑定参数个数完全一致,只需要满足交换机绑定参数即可,也就是说可以传送消息Header属性可以多于交换机绑定参数个数,只需要
             *       传送消息Header属性其中某些属性符合交换机绑定参数的规则就可以了
             */
            Map<String,Object> headerAll = new HashMap<>();
            headerAll.put("x-match","all");
            headerAll.put("flag","consumer");
            headerAll.put("tag","test");
            /**
             * queue: 队列名字
             * exchange:交换机名字
             * routingKey:路由key,当Exchange type是 Header时,可以默认“”
             * arguments:定义绑定参数,目前只有Exchange type是Header时使用到
             */
            channel.queueBind(queueNameAll,EXCHANGE,"",headerAll);

            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueNameAll,false,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));
                    channel.basicAck(envelope.getDeliveryTag(),true);
                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

3 Consumer2


/**
 * @program: rabbitmq-demo
 * @description: 消费者-接受消息  Header any
 **/
public class Consumer_2 {

    private final static String EXCHANGE = "MESSAGE_HEADER";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitmqConfig.getChannel();
            // 声明交换机
            channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.HEADERS);

            String queueNameAny = channel.queueDeclare().getQueue();
            Map<String,Object> headerAny = new HashMap<>();
            headerAny.put("x-match","any");
            headerAny.put("flag","consumer");
            channel.queueBind(queueNameAny,EXCHANGE,"",headerAny);

            /**
             * 消费消息
             * queue:队列名称
             * autoAck:消息确认机制;true:自动确认消息,false:手动确认消息
             * callback:Consumer接口,收到消息后的处理逻辑!这里直接写了一个匿名类
             */
            channel.basicConsume(queueNameAny,false,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException
                {
                    System.out.println("Receive==="+new String(body));
                    channel.basicAck(envelope.getDeliveryTag(),true);
                }
            });
            // 若不关闭connection,则一直保持接受消息的状态
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

可以自己测试一下代码来运行理解一下,毕竟程序员学习的知识还是要体现到代码中,否则也没什么意义。
也可以直接把我写的Demo项目拉下来,运行测试一下。
不知道如何下载GitHub项目的同学,可以查看这个教程首次从github仓库中拉取代码在idea中打开的两种方法

关于Exchange Header内容借鉴于中间件系列七 RabbitMQ之header exchange(头交换机)用法,若看我的文章不明白可以看一下这篇

结语

历时差不多一个月,终于要写到结语了。很不容易,工作中抽出时间真的是各种挤,但是提升技术我认为应该是不管在任何时候都要坚持的,这个也是程序员通往上升的一个基本需要基本的能力。
在我的学习单中,以后还要学习各种技术、各种书籍,目前还只是其中一点点一点点,但是我认为不管多困难的事情,当它放到时间的维度上来说都不再困难,共勉!
对了,关于工作过程中员工个人时间这事,不得不提,国内公司和外企的区别还是很大的,近一两年的目标吧,就是入职一家外企,让我拥有更多的个人时间可以提升时间,当然入职外企的前提也是提升技术,这是个不得不面对的,共勉!

我喜欢写一些啰嗦的话,不好意思,可以跳过,下节就进入到Springboot集成Rabbitmq,在SpringBoot中如何实现消息的发布与订阅呢。

你可能感兴趣的:(Rabbitmq,rabbitmq)