RabbitMQ

102 阅读15分钟

RabbitMQ

1.简单队列

www.rabbitmq.com/tutorials/t…

依赖

<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.6.0</version>
</dependency>

生产者

package com.hoki.simple.send;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class Send {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接工厂的配置
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel())
        {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

消费者

package com.hoki.simple.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

public class Recv {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

1.1 简单队列的优缺点

假设生产者每秒产生100条消息,消费者每秒处理50条消息,那么每秒堆积在消息队列中的就有50条,随着不可控因素的影响,消息队列中的消息可能堆积得越来越多,那么如何处理呢?

2.工作队列(任务队列)

www.rabbitmq.com/tutorials/t…

2.1 轮循调度

  • 生产者
package com.hoki.work.send;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * ClassName:Send
 * Package:com.hoki.simple.send
 * Description:工作队列-轮循-生产者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:04
 */
public class Send {

    private final static String QUEUE_NAME = "work_rx";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接工厂的配置
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            for (int i = 0; i < 20; i++) {
                String msg = "Hello World!-%s";
                String message = String.format(msg, i);
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
                System.out.println(" [x] Sent '" + message + "'");
            }
        }
    }
}
  • 消费者01
package com.hoki.work.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-轮循-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String QUEUE_NAME = "work_rx";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}
  • 消费者02
package com.hoki.work.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-轮循-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv02 {

    private final static String QUEUE_NAME = "work_rx";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

2.1.1 轮循调度的优缺点

工作队列的轮循模式解决了消费者跟不上生产者的问题,但是它的缺点也很明显,比如说第一个消费者(C1)处理一条消息花费1s,第二个消费者(C2)处理一条消息花费3s,那根据以上代码,最终花费150s(木桶原理),在C1处理完它自身的工作量后,便会==造成资源的浪费==,所以可以使用能者多劳的模式(公平模式)来解决这个问题。

轮循调度问题验证代码

结果显示,消费得更快的,更先停止消费操作,所以造成了资源上的浪费

  • 消费者01
package com.hoki.work.fair.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-公平调度-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String QUEUE_NAME = "work_fair";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
            // 模拟线程阻塞
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}
  • 消费者02
package com.hoki.work.fair.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-公平调度-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv02 {

    private final static String QUEUE_NAME = "work_fair";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
            // 模拟线程阻塞
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

2.2 公平调度

  • 消费者01
package com.hoki.work.fair.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-公平调度-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String QUEUE_NAME = "work_fair";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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");

        // 限制消费1条消息,消费完再继续消费下一条消息
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);

        // 打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
            // 模拟线程阻塞
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
             * 手动确认
             * args01: 回调消息的唯一标识
             * args02: 是否确认多条
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };
        // 将自动确认修改为false
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}
  • 消费者02
package com.hoki.work.fair.recv;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.simple.recv
 * Description:工作队列-公平调度-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv02 {

    private final static String QUEUE_NAME = "work_fair";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        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");

        // 限制消费1条消息,消费完再继续消费下一条消息
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);

        // 打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
            // 模拟线程阻塞
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
             * 手动确认
             * args01: 回调消息的唯一标识
             * args02: 是否确认多条
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };
        // 将自动确认修改为false
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}

上述代码,便可实现"能者多劳"的调度,相比"轮循调度"效率要好很多。

3.发布/订阅队列

www.rabbitmq.com/tutorials/t…

  • 生产者
package com.hoki.fanout.send;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * ClassName:Send
 * Package:com.hoki.fanout.send
 * Description:发布/订阅队列-生产者: 将消息发给交换机,而不是队列
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:04
 */
public class Send {

    private final static String EXCHANGE_NAME = "exchange_fanout";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接工厂的配置
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            /*
             * 绑定交换机
             * 1.交换机名称
             * 2.交换机类型(fanout: 广播)
             */
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            String message = "Hello World!";
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}
  • 消费者01
package com.hoki.fanout.recv;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.fanout.recv
 * Description:发布/订阅队列-消费者: 绑定交换机,并将交换机与队列绑定,
 * 生产者将消息发送给交换机后,交换机将消息发给绑定的队列
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String EXCHANGE_NAME = "exchange_fanout";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 绑定交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 声明队列(排他)
        String queueName = channel.queueDeclare().getQueue();
        // 队列和交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");


        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}
  • 消费者02

代码同消费者01

结果: 当生产者发送一条消息到交换机后,消费者都能收到该消息。

4.路由队列

www.rabbitmq.com/tutorials/t…

相比发布/订阅队列, 路由队列需要携带路由key来做区分,来指定哪些队列可以收到指定的消息

  • 生产者
package com.hoki.direct.send;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * ClassName:Send
 * Package:com.hoki.fanout.send
 * Description:路由队列-生产者: 将消息发给交换机,而不是队列
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:04
 */
public class Send {

    private final static String EXCHANGE_NAME = "exchange_direct";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接工厂的配置
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            /*
             * 绑定交换机
             * 1.交换机名称
             * 2.交换机类型(fanout: 广播)
             */
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            String message = "Hello World!";
            // 变换不同的路由key,可以看到只有绑定了指定的路由key才能收到消息
            channel.basicPublish(EXCHANGE_NAME, "green", null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}
  • 消费者01
package com.hoki.direct.recv;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.fanout.recv
 * Description:路由队列-消费者: 绑定交换机,并将交换机与队列绑定,
 * 生产者将消息发送给交换机后,交换机将消息发给绑定的队列,绑定时添加路由key
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String EXCHANGE_NAME = "exchange_direct";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 绑定交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 声明队列(排他)
        String queueName = channel.queueDeclare().getQueue();
        // 队列和交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "black");
        channel.queueBind(queueName, EXCHANGE_NAME, "orange");

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


        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}
  • 消费者02
package com.hoki.direct.recv;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.fanout.recv
 * Description:路由队列-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv02 {

    private final static String EXCHANGE_NAME = "exchange_direct";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 绑定交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 声明队列(排他)
        String queueName = channel.queueDeclare().getQueue();
        // 队列和交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "black");
        channel.queueBind(queueName, EXCHANGE_NAME, "green");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");


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

4.1 路由队列的缺点

随着队列的使用越来越多,路由也随着增多,多到管理难度加大,这个时候就需要使用到主题队列

5.主题队列

www.rabbitmq.com/tutorials/t…

  • *: 匹配一个
  • #: 匹配多个

通过通配符来实现路由管理

  • 生产者
package com.hoki.topic.send;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Send
 * Package:com.hoki.fanout.send
 * Description:主题队列-生产者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:04
 */
public class Send {

    private final static String EXCHANGE_NAME = "exchange_topic";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接工厂的配置
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            /*
             * 绑定交换机
             * 1.交换机名称
             * 2.交换机类型(fanout: 广播)
             */
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
            String message1 = "Hello World!-1";
            String message2 = "Hello World!-2";
            String message3 = "Hello World!-3";
            String routingKey1 = "quick.orange.rabbit";
            String routingKey2 = "lazy.pink.rabbit";
            String routingKey3 = "quick.orange.male.rabbit";


            // 变换不同的路由key,可以看到只有绑定了指定的路由key才能收到消息
            channel.basicPublish(EXCHANGE_NAME, routingKey1, null, message1.getBytes(StandardCharsets.UTF_8));
            channel.basicPublish(EXCHANGE_NAME, routingKey2, null, message2.getBytes(StandardCharsets.UTF_8));
            channel.basicPublish(EXCHANGE_NAME, routingKey3, null, message3.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message1 + "'");
            System.out.println(" [x] Sent '" + message2 + "'");
            System.out.println(" [x] Sent '" + message3 + "'");
        }
    }
}
  • 消费者01
package com.hoki.topic.recv;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.fanout.recv
 * Description:主题队列-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv01 {

    private final static String EXCHANGE_NAME = "exchange_topic";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 绑定交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        // 声明队列(排他)
        String queueName = channel.queueDeclare().getQueue();
        // 队列和交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");


        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}
  • 消费者02
package com.hoki.topic.recv;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

/**
 * ClassName:Recv
 * Package:com.hoki.fanout.recv
 * Description:主题队列-消费者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:10
 */
public class Recv02 {

    private final static String EXCHANGE_NAME = "exchange_topic";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 绑定交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        // 声明队列(排他)
        String queueName = channel.queueDeclare().getQueue();
        // 队列和交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
        channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");


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

6.RPC队列

  • RPCClient
package com.hoki.rpc.client;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;

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 {
        final String corrId = UUID.randomUUID().toString();

        String replyQueueName = channel.queueDeclare().getQueue();
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", requestQueueName, props, message.getBytes(StandardCharsets.UTF_8));

        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(), StandardCharsets.UTF_8));
            }
        }, consumerTag -> {
        });

        String result = response.take();
        channel.basicCancel(ctag);
        return result;
    }

    @Override
    public void close() throws IOException {
        connection.close();
    }
}
  • RPCServer
package com.hoki.rpc.server;

import com.rabbitmq.client.*;

import java.nio.charset.StandardCharsets;

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(), StandardCharsets.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(StandardCharsets.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();
                    }
                }
            }
        }
    }
}

7.事务

消费者消费完一条消息后可以自动确认或者手动确认,但是生产者生产一条消息后,发送到消息队列里如何确认却是个问题。

  • 通过AMQP事务机制实现
  • 通过将channel设置成confirm模式来实现

7.1 AMQP事务机制控制

RabbitMQ中与事务机制有关的方法有三个: txSelect(), txCommit()以及txRollback(), txSelect()用于将当前channel设置为transaction模式, txCommit()用于提交事务,txRollback()用于回滚事务, 在通过txSelect()开启事务之后,我们可以发布消息给broker代理服务器了,如果txCommit()提交成功了,则消息一定到达了broker了,如果在txCommit()执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback()回滚事务。

其实,RabbitMQ事务并不经常使用到, 首先是它相比关系型数据库的事务来说,它的强一致性并不强,其次,是它会降低性能,但是,RabbitMQ的事务确实能解决生产者是否真的将消息发送到服务器的问题。

8.confirm确认模式

通过AMQP协议层面为我们提供了事务机制解决了生产者发送消息到服务器的确认问题,但是采用事务机制会降低RabbitMQ的消息吞吐量,此时,处理AMQP协议层面能够实现消息事务控制外,还有第二种方式,即: Confirm模式。

8.1 Confirm确认模式原理

生产者将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外,broker也可以设置basic.ack的multiple域, 表示到这个序列号之前的所有消息都已经得到了处理。

confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。

在channel被设置成confirm模式之后,所有被publish的后续消息都将被confirm(即ack)或者被nack一次。但是,没有对消息被confirm的快慢做任何保证,并且同一条消息不会既被confirm又被nack。

注意: 两种事务控制机制不能同时开启。

8.2 Confirm确认机制的代码实现

实现生产者confirm机制有三种方式:

  1. 普通confirm模式: 每发送一条消息后, 调用waitForConfirms()方法,等待服务器端confirm,实际上是一种串行confirm了
  2. 批量confirm模式: 每发送一批消息后, 调用waitForConfirmsOrDie()方法,等待服务器confirm
  3. 异步confirm模式: 提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方法

9.异步确认

异步confirm模式的编程实现最复杂,Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Channel发出的消息序号),我们需要自己为每个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个unconfirm集合最好采用有序集合SortedSet存储结构,实际上,waitForConfirms()方法也是通过SortedSet维护消息序号的。

10.Spring AMQP

docs.spring.io/spring-amqp…

11.消息的可靠性投递

指的是生产者发送的消息一定能被消费者接收到

11.1 消息发送过程可能丢失的步骤

  1. 生产者->交换机: 使用==confirm机制==仅能保证生产者生产的消息能否发送到交换机
  2. 交换机->队列: 使用==return机制==
  3. 队列->消费者: 把自动ack改为手动ack

如果交换机和队列没有设置持久化属性(durable),当RabbitMQ重启后,交换机和队列将丢失。

  • confirm机制
package com.hoki.confirm.sync.send;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

/**
 * ClassName:Send
 * Package:com.hoki.simple.send
 * Description:确认-同步-生产者
 *
 * @author :Hoki_Lin
 * @date :2022/7/23 18:04
 */
public class Send {

    private final static String QUEUE_NAME = "sync";

    public static void main(String[] argv) throws Exception {
        // 连接工厂的配置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.77.77.238");
        factory.setPort(5672);
        factory.setUsername("alpacanote");
        factory.setPassword("5bgeuPOS6WG94awO");
        factory.setVirtualHost("alpacanote");

        Channel channel = null;
        try (Connection connection = factory.newConnection()) {
            channel = connection.createChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);

            // 开启confirm机制
            channel.confirmSelect();
            // 设置confirm监听器
            channel.addConfirmListener(new ConfirmListener(){

                // 当消息被broker签收了,会回调此方法
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("消息已经成功投递");
                }
                // 当消息没有被broker签收了,会回调此方法
                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    // 开启重试机制,重试达到阈值,则人工介入
                    System.out.println("消息投递失败");
                }
            });

            // 发送消息
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());


            // 确认消息
            // 普通确认,只能单条确认
            if (channel.waitForConfirms()) {
                System.out.println("确认成功!");
            }

            // 批量确认, 只要有一条确认不成功直接抛异常
            channel.waitForConfirmsOrDie();

            System.out.println(" [x] Sent '" + message + "'");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 如果开启了confirm机制,则不要关闭channel,不然接受不到消息
//            if (channel != null) {
//                channel.close();
//            }
        }
    }
}

12.保证消息的幂等性消费

生产者发送消息时,消息头携带id,结合redis分布式锁可以保证消息的幂等性消费

13.死信队列

死信队列,让一条消息在满足一定条件下,成为死信,会被发送到另一个交换机上,再被消费,这个过程就是死信队列的作用,死信队列就可以做出延迟队列的效果,比如,在订单超时未支付,将订单状态改成"已取消",这个操作就可以使用死信队列来完成。设置消息的超时时间,当消息超时则消息成为死信,于是, 通过监听死信队列的消费者来做取消订单的操作。

13.1 消息如何成为死信?

  1. 消息被拒签,并且没有重回队列,消息将成为死信
  2. 消息过期了,消息将成为死信
  3. 队列长度有限,存不下消息了,存不了的信息将会成为死信

13.2 如何创建死信队列?