RabbitMQ实战之消息队列Queue

孤街浪徒 提交于 2019-12-09 17:30:45

 
 我的上一篇RabbitMQ初识文章:RabbitMQ实战之初识篇
 RabbitMQ官方讲解教程(推荐学习):RabbitMQ Tutorials
 RabbitMQ-Tutorials官方GitHub代码仓库(多种语言均有):rabbitmq-tutorials
 

=》RabbitMQ常用的几种Queue(消息队列)教程

          我的实验是基于 [ JDK1.8 + Maven + IDEA ],所以基于Maven的基础上以下队列项目的workspace需要引入如下的 pom.xml 中的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xxx</groupId>
    <artifactId>RabbitMQ</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <!-- configuration配置跟JDK版本相关联 -->
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- rabbitmq客户端依赖 -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.1</version>
        </dependency>
        
        <!-- rabbitmq客户端依赖(上面的)依赖于slf4j依赖 -->
        <!-- 但在生产环境中(线上)应该引入更加成熟的日志依赖库,如Logback日志库 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.26</version>
        </dependency>
    </dependencies>
</project>

 

一、 Hello World

     Hello World
  P:Producer - 生产者:生产msg并发布到队列(queue)中
  C:Consumer - 消费者:保持运行 && 监听队列 中的msg,拉取msg并消费它

  • 生产者发送msg到mq的代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class Send {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // mq的连接工厂
        ConnectionFactory factory = new ConnectionFactory(); 
        // mq的服务器地址,这里是本机IP 127.0.0.1
        // 也可以设置其他信息:端口port、用户名、密码passwd等
        factory.setHost("localhost"); 
        // 从factory中获取连接 + 在连接中创建管道 channel
        // channel: 完成工作的大多数API所在的位置
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 在管道中声明连接的队列 queue
            // Declaring a queue is idempotent[声明队列是幂等的,仅当队列不存在时才创建]
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "HELLO MQ-queue, i am hengx's Producer";
            // 发布(Publish) msg 到 queue
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("[x] Send '" + message + "'");
        }
    }
}
  • 消费者从mq中拉取并消费msg的代码:
public class Recv {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
		// TODO
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        // 从队列(queue)监听、拉取并消费(Consume) msg
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

运行生产者后管理界面查看:
mq
点击队列的名字即可查看详细的信息:
mq

 1) 为什么不使用try语句自动关闭管道Channel跟连接Connection呢?(即调用chanel.close();connection.close();)
      如果这样,我们的程序将继续运行,然而关闭了所有内容 (管道 + 连接) 并退出!这将很尴尬,因为我们希望在消费者 异步侦听消息到达 时,该过程保持有效

 2) queueDeclare() 参数解析:

     我的下一篇博文:channel.queueDeclare()参数解析
 

二、 Work queues

     work-queue
一个生产者,多个消费者 ;
一个msg只能被一个消费者消费
msg在消费者之间是共享的 ;

1. 目的:
        To avoid doing a resource-intensive(资源密集型) task immediately and having to wait for it to complete. Instead we schedule the task to be done later. [ 就是避免立即处理资源密集型的任务而是在后期处理它 ,异步处理达到削峰的目的],比如在一个HTTP短连接中要处理一个长时间的任务是不可能的,特别是在Web应用中.

  • 生产者代码:
public class Send_WorkQueue {

    private static final String QUEUE_NAME = "work-queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            args = new String[]{"Java",".",".","."};
            String message = String.join(" ",args);
            // 发送20条消息到 mq
            for (int i=0;i<20;i++){
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            }
            System.out.println("[x] Send '" + message + "'");
        }
    }
}
  • 消费者-1 代码:
public class Recv1_WorkQueue {
    private final static String QUEUE_NAME = "work-queue";
    private static int count = 0;

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x1] Received '" + message + "'");
            try {
            	// 模拟耗时工作
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("[x1] Done,count : " + (++count));
            }
        };
        // false关闭自动确认ack
        boolean autoAck = true; // acknowledgment is covered below
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) throws InterruptedException {
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(100);
            }
        }
    }
}
  • 消费者-2 代码(跟消费者1不同之处在于doWork()函数的模拟任务处理时间不同):
public class Recv2_WorkQueue {
    private final static String QUEUE_NAME = "work-queue";
    private static int count = 0;

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x1] Received '" + message + "'");
            try {
            	// 模拟耗时工作
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("[x1] Done,count : " + (++count));
            }
        };
        // false关闭自动确认ack
        boolean autoAck = true; // acknowledgment is covered below
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) throws InterruptedException {
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(1000);
            }
        }
    }
}

测试结果:

  • 消费者1跟消费者2得到的消息msg数量是相同的 ;
  • 那么这就有问题了,消费者2的任务耗时比消费者1要多10倍,就是说消费者1做完之后空闲着,而消费者2就很繁忙,显然这是不符合实际的。那么该怎么解决这个问题呢,学习到【第4点-Fair dispatch (公平分发msg)】就能解决这个问题 。

 
2. Message acknowledgment (消息确认):

  1) 自动确认
       当开启自动确认时,即autoAck = true,RabbitMQ默认发送完消息给消费者后自动确认为消费完成并从队列queue中删除该msg, 即使消费者端的msg丢失导致任务失败,RabbitMQ也不知道它的状态,默认消费完成;

  2) 手动确认

  • 手动确认的流程:An ack(nowledgement) is sent back by the consumer to tell RabbitMQ that a particular message has been received, processed and that RabbitMQ is free to delete it.

  • 为啥需要手动确认:If a consumer dies (its channel is closed, connection is closed, or TCP connection is lost) without sending an ack, RabbitMQ will understand that a message wasn’t processed fully and will re-queue (重新排队) it. If there are other consumers online at the same time, it will then quickly redeliver it to another consumer. That way you can be sure that no message is lost, even if the workers occasionally die.

  • 怎么实现消息手动确认

		// 同一时刻服务器只会发一条消息给同一个消费者
	    channel.basicQos(1); // accept only one unack-ed message at a time (see below);
	    // 开启手动确认模式
	    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
	    // autoAck = false 代表手动确认msg的完成状态
	    boolean autoAck = false;
		channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

 

3. Round-robin dispatching (轮询分发msg, RabbitMQ 默认方式):
      By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages.[ 默认情况下,RabbitMQ会将queue中的msg顺序地、平均地分发给其所绑定的消费者,每个消费者最后获得的msg数量是相同的 ]
 

4. Fair dispatch (公平分发msg, 非RabbitMQ 默认方式)
      上述的例子所运行的结果中,两个消费者获得的msg的数量是相同的,但是会出现其中一个消费者比较闲,另外一个消费者比较忙的情况,从而造成了资源的不完全利用问题 ;
      为了解决这个问题,我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送 ;
      还有一点需要注意,使用公平分发,必须关闭自动应答,改为手动应答

      设置公平分发msg的代码就是跟手动消息确认的代码联合使用的:

		// 同一时刻服务器只会发一条消息给同一个消费者
	    channel.basicQos(1); // accept only one unack-ed message at a time (see below);
	    // 开启手动确认模式
	    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
	    // autoAck = false 代表手动确认msg的完成状态
	    boolean autoAck = false;
		channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
  • 生产者代码:
public class Send_WorkQueue {

    private static final String QUEUE_NAME = "work-queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            args = new String[]{"Java",".",".","."};
            String message = String.join(" ",args);
            //channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            // 将消息标记为持久消息
            // 发送20条消息
            for (int i=0;i<20;i++){
                channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
            }
            System.out.println("[x] Send '" + message + "'");
        }
    }
}
  • 消费者-1代码:
public class Recv1_WorkQueue {
    private final static String QUEUE_NAME = "work-queue";
    private static int count = 0;

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        // 同一时刻服务器只会发一条消息给消费者
        //一次仅接受一条未经确认的消息
        channel.basicQos(1);

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x1] Received '" + message + "'");
            try {
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("[x1] Done,count : " + (++count));
                //开启这行 表示使用手动确认ack模式
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        // false关闭自动确认ack,改为手动确认ack
        boolean autoAck = false; // acknowledgment is covered below
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) throws InterruptedException {
        int count = 1;
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(100);
                //System.out.println("Task-module-" + (count++) + " Done");
            }
        }
    }
}

  • 消费者-2代码:
public class Recv2_WorkQueue {
    private final static String QUEUE_NAME = "work-queue";
    private static int count = 0;

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        channel.basicQos(1); //一次仅接受一条未经确认的消息
        
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x2] Received '" + message + "'");
            try {
                doWork(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("[x2] Done,count : " + (++count));
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        boolean autoAck = false; // acknowledgment is covered below
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) throws InterruptedException {
        int count = 1;
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(1000);
                //System.out.println("Task-module-" + (count++) + " Done");
            }
        }
    }
}

测试结果:

  • 消费者1获得的msg数量比消费者2获取的多;
     

5. Message durability(消息的持久性)
     When RabbitMQ quits or crashes it will forget the queues and messages unless you tell it not to. Two things are required to make sure that messages aren’t lost: we need to mark both the queue and messages as durable. [ 意思就是当RabbitMQ服务停止或者服务奔溃的时候它将丢失所有的队列和消息msg,除非同时设置队列跟消息是 持久的 ] .

     开启队列queue跟消息msg持久性:

boolean durable = true;
channel.queueDeclare("work-queue", durable, false, false, null);

channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

 

三、 Publish / Subscribe

     Publish / Subscribe

  • 在 work-queue 模式中,生产者将消息直接 发布到消息队列,且一个消息只能被一个消费者消费 ;
  • 假如现在有10W个订阅者在CSDN订阅某个博主的文章,当博主发布一篇文章时,为了将文章推送到每个订阅者,由于每个消息msg只能被一个消费者消费,那么如果用 work-queue 模式就要往队列中生产10W条一模一样的消息,显然,这是不现实的 ; Publish / Subscribe 就是解决这个问题的一个模式 ;
  • 发布 / 订阅模式:该模式类似于在CSDN / 知乎上订阅了某个博主的文章,当博主发布文章时推送到 所有 订阅者;
  • Exchanges [ 交换机,图中的X ]: 接收生产者发来的msg然后 扇出 到每个绑定的队列中。若交换机没有绑定队列,msg将会丢失,因为交换机Exchanges不存储消息,只是转发消息
  • 1个生产者,多个消费者 ;
  • 每个消费者都有自己的一个队列queue ;
  • 生产者没有将消息直接发送到队列,而是交换机 [X] ,所以生产者并不知道消息最后到底有没有到达队列中 ;
  • 每个队列都要绑定到交换机中 ;
  • 1个消息msg,通过交换机后到达每个绑定的队列中,实现 同一个消息被多个消费者消费 的目的 ;
  • Exchanges (交换机) 有4种类型:direct,topic,headers,fanout ;在Publish / Subscribe 模式中对应的是 fanout 模式,即扇出模式,将一个消息扇出到所有队列中 ;

生产者代码:

public class EmitLog {

    private final static String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接Connection
        // 2.创建管道channel
        ConnectionFactory cf = new ConnectionFactory();
        cf.setHost("localhost");
        try (Connection connection = cf.newConnection();
             Channel channel = connection.createChannel();) {
             
            // 3.定义交换机Exchange && 发布消息到交换机中
            // 指定交换机的类型Type为FANOUT
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            String message = "runtime logs...";
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println("[x] Send '" + message + "'");
        }

    }
}

消费者-1 代码(接收并打印日志):

public class RecvLogs {

    private final static String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        //临时队列 - it will be deleted when no consumer bind it
        String QUEUE_NAME = channel.queueDeclare().getQueue();
        //channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //将队列绑定到交换机中
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received and Print '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
        });

    }
}

消费者-2 代码(存储日志到 disk):

public class SaveLogs {

    private final static String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        //临时队列 - it will be deleted when no consumer bind it
        String QUEUE_NAME = channel.queueDeclare().getQueue();
        //channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //将队列绑定到交换机中
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received and Save '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
        });

    }
}

测试结果:

  • 同一个消息msg会被所有消费者获取并消费 ;
  • 若同一个消费者队列绑定了多个消费者,则同一个消息msg只会被其中一个消费者消费 ;
  • 一处代码解析:
String QUEUE_NAME = channel.queueDeclare().getQueue();
=》 当我们不向queueDeclare()提供任何参数时,我们将使用生成的名称创建一个非持久的,
=》 排他的,自动删除的队列--> 当服务器程序停止运行时, 交换机跟绑定在该交换机的所有队列自动解绑

=》好处是什么:只关注当有消息要消费时且生产者跟消费者不用绑定同一个队列时使用,节省队列保持运行的消耗
  • 查看绑定关系:
    FANOUT

 

四、 Routing

     Routing-Type

  • 上一个模式 [ Publish/Subscribe ] 即扇出模式,交换机将消息发布到所有绑定的队列中,是一种 盲发 的模式,这种交换并没有给我们带来太大的灵活性-它只能进行 无意识的广播
  • 路由模式(即直接交换模式),相当于网络IP寻址;在路由模式 [Routing] 中,每个消息带有一个路由秘钥,每个绑定在交换机上的队列也绑定了一个秘钥,消息只会进入队列的绑定密钥与消息的路由密钥完全匹配的队列
  • 当消息msg的密钥跟绑定在direct-exchange (直接交换机) 的任何一个queue的 密钥匹配不上时, 该msg将会discard(消失)
  • 该模式下的交换机的类型是 DIRECT
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
  • 声明消息msg的路由密钥:
// 注意第二个参数severity: 查看源码得知第二个参数就是--> routing-key
String severity = "error";
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
  • 声明绑定在交换机exchange的队列的绑定密钥:
// 第3个参数就是设定队列密钥的
String severity = "error";
channel.queueBind(queueName, EXCHANGE_NAME, severity);

生产者代码:

public class EmitLogDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            argv = new String[]{"error","hengx's mq-server"," exploded.", " please maintain"};

            // severity is a routing-key
            String severity = getSeverity(argv);
            String message = getMessage(argv);

            // 注意第二个参数severity: 查看源码得知第二个参数就是--> routing-key
            channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
        }
    }

    private static String getSeverity(String[] strings) {
        if (strings.length < 1)
            return "info";
        return strings[0];
    }

    private static String getMessage(String[] strings) {
        if (strings.length < 2)
            return "Hello World!";
        return joinStrings(strings, " ", 1);
    }

    private static String joinStrings(String[] strings, String delimiter, int startIndex) {
        int length = strings.length;
        if (length == 0) return "";
        if (length <= startIndex) return "";
        StringBuilder words = new StringBuilder(strings[startIndex]);
        for (int i = startIndex + 1; i < length; i++) {
            words.append(delimiter).append(strings[i]);
        }
        return words.toString();
    }
}

消费者-1 代码:

public class ReceiveLogsDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = channel.queueDeclare().getQueue();
		
		// 队列绑定的密钥数组
        argv = new String[]{"info","warning","error"};
        if (argv.length < 1) {
            System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
            System.exit(1);
        }

        // severity is the binding-key
        for (String severity : argv) {
            channel.queueBind(queueName, EXCHANGE_NAME, severity);
        }
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

消费者-2 代码:

消费者-2 的代码类似于消费者-1,就是队列绑定的密钥没有 "error" 

测试结果:

  • 生产者发布的msg会进入消费者-1的队列并被消费者-1消费,不会进入消费者-2的队列 ;

注意:

  • 一个队列可以绑定多个密钥
  • 不同队列的绑定密钥可以相同 ,当msg的路由密钥匹配时交换机会将该消息发送到不同队列中。比如上面的Routing图示中两个queue都绑定了 "error" 密钥,当一个路由密钥为 error 的消息发布到交换机时,交换机会将该消息同时发送给这两个队列 ;

 

五、 Topics

     Topics

  • 路由模式 (Routing) 中,可以根据路由密钥将消息发送到对应的队列给消费者消费;假如现在想要将 “error” 消息中由redis服务器产生的发送到一个队列,“error” 消息中由 mysql服务器产生的发送到另一个队列,而所有服务器报的 “error” 消息是 由多个节点信息组成 (用 “.” 分开) 而不仅仅是 “error” 标识,比如 "error.redis.master.master-2 is exploded""mysql.error.ORA-00227: corrupt block detected in control file: (block 0, # blocks )",这种情况下使用Routing模式是无法实现这个功能的,此时就应该用到Topic模式 ;

  • Topic (即通配符模式): it can do routing based on multiple criteria [它能根据多重标准进行路由],就是在Routing模式上多加一些条件进行筛选 ;

  • 发送到Topic交换机(Topic-Exchange) 的消息不能具有任意的routing_key- 它必须是单词列表,以点 “.” 分隔,比如:quick.orange.rabbit ;

  • 消息路由密钥中可以包含任意多个单词,最多255个字节

  • 队列的绑定密钥重要组成规则:
      1) * (star) 匹配一个单词,如 *.* 可以匹配 redis.cluster,不能匹配 redis.aa.bb
      2) # (hash) 匹配一个或多个单词,如 redis.# 可以匹配 redis.aa.bb.cc.dd,不能匹配 redis

  • msg的 路由密钥队列的绑定密钥 匹配不上时msg将会丢失;

  • msg的 路由密钥组成形式 必须跟 队列的绑定密钥规则 相同,否则msg也会丢失,eg (队列的绑定密钥为 " * .* .* "):
            What happens if we break our contract and send a message with one or four words, like “orange” or “quick.orange.male.rabbit”? Well, these messages won’t match any bindings and will be lost.

规则这么多,那就来个demo看一下:
Exchange-Type:Topic's Demo

  • 生产者代码:
    public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            argv = new String[]{"kern.critical.hengx", "A critical kernel error in hengx's mq-server"};

            String routingKey = getRouting(argv);
            String message = getMessage(argv);

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
        }
    }

    private static String getRouting(String[] strings) {
        if (strings.length < 1)
            return "anonymous.info";
        return strings[0];
    }

    private static String getMessage(String[] strings) {
        if (strings.length < 2)
            return "Hello World!";
        return joinStrings(strings, " ", 1);
    }

    private static String joinStrings(String[] strings, String delimiter, int startIndex) {
        int length = strings.length;
        if (length == 0) return "";
        if (length < startIndex) return "";
        StringBuilder words = new StringBuilder(strings[startIndex]);
        for (int i = startIndex + 1; i < length; i++) {
            words.append(delimiter).append(strings[i]);
        }
        return words.toString();
    }
}
  • 消费者代码:
public class ReceiveLogsTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        // 以下代表不同的 binding-key匹配规则
        //argv = new String[]{"#"};
        argv = new String[]{"kern.*.hengx"};
        //argv = new String[]{"*.critical.hengx"};
        //argv = new String[]{"kern.*.*", "*.critical.*"};

        for (String bindingKey : argv) {
            channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
        }

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

注意:
  1) 当队列的绑定密钥 [ binding-key ] 是 "#" 时,不管消息的路由密钥是什么,该队列将接收所有从交换机发送的消息msg,类似于 [ Publish / Subscribe ] 扇出模式 ;

  2) 当队列的绑定密钥 [ binding-key ] 没有 "*""#" 符号 时,它将变为 路由模式

 

六、 RPC

     
RabbitMQ-RPC

1. 基础概念

  • RPC:Remote Procedure Call,即 远程过程调用
  • RPC应用场景:当客户端需要调用服务端并 等待 结果返回时;
  • 在之前的MQ模式中都是通过将消息丢给队列供其他消费者消费,生产者并不关注也不要求得到消费者的消费结果,所以是无法用来做RPC的 ;
  • 使用MQ做RPC的模式图如上图所示流程图 ;

2. RPC调用流程

(1) 对于RPC请求,客户端发送一条消息,该消息具有两个属性:replyTo(设置为仅为请求创建的匿名独占队列)和 correlationId(设置为每个请求的唯一值);

        replyTo属性:Callback-Queue,请求的回调队列,客户端要等待接收服务器端的执行结果,就要告诉服务器端将结果发送到哪里 :

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();

channel.basicPublish("", "rpc_queue", props, message.getBytes());

        correlationId属性:

  • 如果我们为每个RPC请求创建一个回调队列。那是相当低效的,但是幸运的是有更好的方法-让我们 为每个客户端创建一个回调队列。
  • 这引起了一个新问题,在该队列中收到响应后,尚不清楚响应属于哪个请求。那就是当使用correlationId属性时,我们将为每个请求将其设置为 唯一值稍后,当我们在回调队列中收到消息时,我们将查看该属性,并 基于此属性将响应与请求进行匹配。如果我们 看到一个未知的correlationId值,我们可以放心地 丢弃 该消息-它不属于我们的请求。
  • 为什么我们应该忽略回调队列中的未知消息,而不是因错误而失败?这是由于服务器端可能出现竞争状况。尽管可能性不大,但RPC服务器可能会在向我们发送答案之后但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端上我们必须妥善处理重复的响应,并且理想情况下RPC应该是 幂等的
AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

(2) 请求被发送到rpc_queue队列 ;
(3) RPC工作程序(又名:服务器)正在等待该队列上的请求。当服务器端发现请求到达队列rpc_queue时,它将处理该请求,然后通过replyTo()属性设置的回调队列将执行结果返回给客户端 ;
(4) 客户端等待回调队列中的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则将响应返回给应用程序。

3. Client && Server代码

  • RPC客户端代码:
public class RPCClient implements AutoCloseable {

    private Connection connection;
    private Channel channel;
    private String requestQueueName = "rpc_queue";

    public RPCClient() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();
    }

    public static void main(String[] argv) {
        try (RPCClient fibonacciRpc = new RPCClient()) {
            for (int i = 0; i < 32; i++) {
                String i_str = Integer.toString(i);
                System.out.println(" [x] Requesting fib(" + i_str + ")");
                String response = fibonacciRpc.call(i_str);
                System.out.println(" [.] Got '" + response + "'");
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String call(String message) throws IOException, InterruptedException {
    	// 产生一个随机的ID
        final String corrId = UUID.randomUUID().toString();

        String replyQueueName = channel.queueDeclare().getQueue();
        // 设置消息msg属性
        // 消息有14个常用的属性,可查看BasicProperties类源代码学习
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

		// BlockingQueue的作用: 因为该demo是在单线程环境下处理的,所以在等待服务端的
		// 响应到达之前,需要挂起客户端主线程,使用BlockingQueue的tack()方法的等待
		// 阻塞原理可以挂起客户端主线程,让call()方法等待结果的到来再返回给主线程main()
        final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);

            String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                response.offer(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });

		// 等待阻塞
        String result = response.take();
        // 当该请求的结果到达时,显示地取消该消费者
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

  • RPC服务端代码:
public class RPCServer {

    private static final String RPC_QUEUE_NAME = "rpc_queue";

	// 斐波那契数列的计算
    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 {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
            // 清空队列中的内容,但不删除队列本身
            channel.queuePurge(RPC_QUEUE_NAME);

            channel.basicQos(1);

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

            Object monitor = new Object();
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();

                String response = "";

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

                    System.out.println(" [.] fib(" + message + ")");
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e.toString());
                } finally {
                    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 -> {
            }));
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

 
Mark:
       在该demo学习中,仅仅是了解一下RabbitMQ可以用来实现RPC,实例比较简单。而在真正应用中嗨呀考虑很多复杂的问题,诸如:

  • 如果客户端存在,而 服务器端不存在 ,客户端该怎么react ?
  • 客户端调用远程服务端出现超时(TimeOut)怎么办 ?
  • 服务端出现 故障 或者 抛出异常Exception,要不要发送该信号给客户端 ?
  • 在处理之前防止无效的传入消息(例如检查边界,类型)

 
 
后记:
     博主也是在边学习边将RabbitMQ的知识整理出来,希望能帮助到大家,一起相互学习,对文章中有什么问题或者建议欢迎交流 。最后推荐养成看官网学习的好习惯(虽然是英文的 <* _ *>)

 
 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!