消息队列之RabbitMQ快速入门及深入

1.RabbitMQ简介

RabbitMQ基础架构:
在这里插入图片描述
Broke: 就是RabbitMQ服务器,生产者和消费者连接的对象。
Virtual Host:理解为服务器上不同的账户,互相隔离。
Exchange:交换机,用于将用户的消息 按照 用户发送来的路由key 传递对应的队列中,注意其只负责转发消息
Queue: 生产者发送消息最终都会到队列中,生产者可以直接绑定该队列,也可以通过交换机与队列之间的绑定,由交换机将消息传递到队列上,以供消费者拉取消费。
Binding: 队列和交换机之间的绑定规则。
Connection: 生产者和消费者 需要和 Broker 之间建立TCP连接。
channel:connection内部的建立的逻辑连接,可用于多线程,减少每次都要connection的消耗。

2.Rabbit快速入门

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

2.1 简单模式

在简单模式中,只有生成者、队列、消费者,生产者直接将消息发送到队列中,消费者拉取消息。
在这里插入图片描述
生产者代码:创建连接 -> 创建通道 -> 创建队列(已存在,则用已存在的队列)-> 向队列中发送消息 -> 释放资源。

	public static void main(String[] args) throws Exception {
	    //1. 创建连接工厂
	    ConnectionFactory factory = new ConnectionFactory();
	    //2. 设置参数
	    factory.setHost("localhost");   // ip 默认值 localhost
	    factory.setPort(5672);      // 端口 默认值 5672
	    factory.setVirtualHost("/");  // 虚拟机 默认值/
	    factory.setUsername("zsk"); // 用户名  默认 guest
	    factory.setPassword("123456");  // 密码  默认值 guest
	
	    //3. 创建连接 Connection
	    Connection connection = factory.newConnection();
	    //4. 创建Channel
	    Channel channel = connection.createChannel();
	    //5. 创建队列Queue
	    /**
	    * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
	    *      1.queue: 队列名称
	    *      2.durable: 是否持久化,当mq重启之后,数据还在
	    *      3.exclusive:
	    *          * 是否独占,只能有一个消费者监听这队列
	    *          * 当connection关闭时,是否删除队列
	    *      4.autoDelete: 是否自动删除,当没有Consumer时,自动删除掉
	    *      5.arguments: 参数
	    */
	    // 如果没有hello_world队列,则会创建,如果有则不会创建
	    channel.queueDeclare("hello_world", true, false, false, null);
	    //6. 发送消息
	    /**
	    *  String exchange, String routingKey, AMQP.BasicProperties props, byte[] body
	    *      1.exchange: 交换机名称。 简单模式下交换机会使用默认的 ""
	    *      2.routingKey: 路由名称  (有hello_world队列就会绑定)  -- 后面具体讲
	    *      3.props: 配置信息
	    *      4.body: 发送消息数据
	    */
	    String body = "hello rabbitmq~~~~";
	    channel.basicPublish("", "hello_world", null, body.getBytes());
	
	    // 7.释放资源
	    channel.close();
	    connection.close();
	}

生产者代码:创建连接 -> 创建通道 -> 创建队列 -> 接收消息
在实际开发中,一般通过监听者来实现接收消息,不用关闭资源。

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("zsk");
        factory.setPassword("123456");

        //3. 创建连接 Connection  -- 与生产者Connection是不一样的
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //5. 创建队列Queue  (生产者已经创建了,这里可以不用)
        channel.queueDeclare("hello_world", true, false, false, null);


        // 接收消息对象
        Consumer consumer = new DefaultConsumer(channel){
            /**
             * 回调方法,当收到消息后,会自动执行该方法
             * @param consumerTag   标识
             * @param envelope      获取一些信息,交换机,路由key...
             * @param properties    配置信息
             * @param body          数据
             */
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag: " + consumerTag);
                System.out.println("Exchange: " + envelope.getExchange());
                System.out.println("RoutingKey: " + envelope.getRoutingKey());
                System.out.println("properties: " + properties);
                System.out.println("body: " + new String(body));    // 字节数组转换为字符串
            }
        };
        /** basicConsume(String queue, boolean autoAck, Consumer callback)
         *     参数:1.queue:      队列名称
         *          2.autoAck:    是否自动确认
         *          3.callback:   回调对象
         */
        channel.basicConsume("hello_world", true, consumer);

        // 消费者一般不关闭资源
    }

2.2 工作队列模式

本质和简单模式区别不大,无非是多启动几个客户端,然后这个客户端竞争这个队列中的消息。默认是轮流的一种算法。
在这里插入图片描述
优点:消费者多可以提高消费处理速度,如:短信服务器可以部署多个等。
代码上同,多复制几个客户端即可。

2.3 发布订阅模式

上面两种模式,同一个消息只会被一个消费者接收,这里可以每个消息可以被多个消费者消费了,同时这里需要用到交换机了。
在这里插入图片描述
发布订阅模式流程:消费者将消息发送给交换机,同时设置路由key为""(因为将消息发送到所有被绑定的队列上,不需要绑定规则),每个消费者拉取队列中消息即可。
每个消费者都可以获取消息的好处在于可以分别做不同的处理,如一个打印到控制台,另一个将消息保存到数据库等
生产者代码

public class Producer_PubSub {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/itcast");
        factory.setUsername("zsk");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /**
         * exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
         * 参数:
         *      1.exchange: 交换机
         *      2.type: 交换机类型
         *          DIRECT("direct"),   :定向
         *          FANOUT("fanout"),   :扇形(广播),发送消息到每一个与之绑定队列。
         *          TOPIC("topic"),     :通配符的方式
         *          HEADERS("headers"); :参数匹配
         *      3.durable:是否持久化
         *      4.autoDelete:自动删除
         *      5.internal:内部使用,一般false
         *      6.arguments:参数
         */
        String exchangeName = "test_fanout";
        // 5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
        // 6.创建队列
        String queue1Name = "test_fanout_queue1";
        String queue2Name = "test_fanout_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 7.绑定队列和交换机
        /**
         * queueBind(String queue, String exchange, String routingKey)
         * 参数:
         *      1.queue:        队列名称
         *      2.exchange:     交换机名称
         *      3.routingKey:   路由键,绑定规则
         *          如果交换机的类型为fanout,routingKey设置为""
         */
        channel.queueBind(queue1Name, exchangeName, "");
        channel.queueBind(queue2Name, exchangeName, "");

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

消费者1代码:

public class Consumer_PubSub1 {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/itcast");
        factory.setUsername("zsk");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        String queue1Name = "test_fanout_queue1";
        String queue2Name = "test_fanout_queue2";

        /** basicConsume(String queue, boolean autoAck, Consumer callback)
         *     参数:1.queue:      队列名称
         *          2.autoAck:    是否自动确认
         *          3.callback:   回调对象
         */
        // 接收消息
        Consumer consumer = new DefaultConsumer(channel){
            /**
             * 回调方法,当收到消息后,会自动执行该方法
             * @param consumerTag   标识
             * @param envelope      获取一些信息,交换机,路由key...
             * @param properties    配置信息
             * @param body          数据
             */
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body: " + new String(body));    // 字节数组转换为字符串
                System.out.println("将日志信息打印到控制台.....");
            }
        };
        channel.basicConsume(queue1Name, true, consumer);
    }
}

消费者2代码:

        Consumer consumer = new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body: " + new String(body));    // 字节数组转换为字符串
                System.out.println("将日志信息保存到数据库.....");
            }
        };
        channel.basicConsume(queue2Name, true, consumer);

2.4 Routing路由模式

在发布订阅模式中,控制台可以打印出任何级别的消息,而数据库完全没有必要存 Info 级别的日志消息。
路由模式:交换机可以根据路由key,将消息发送到不同队列,从而可以确保特定消息做特定的事
在这里插入图片描述
生产者代码

public class Producer_Routing {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/itcast");
        factory.setUsername("zsk");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /**
 			交换机类型: DIRECT("direct"):定向
         */
        String exchangeName = "test_direct";
        // 5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);
        // 6.创建队列
        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 7.绑定队列和交换机
        /**
         * queueBind(String queue, String exchange, String routingKey)
         * 参数:
         *      1.queue:        队列名称
         *      2.exchange:     交换机名称
         *      3.routingKey:   路由键,绑定规则
         *          如果交换机的类型为fanout,routingKey设置为""
         */
        // 队列1绑定 error
        channel.queueBind(queue1Name, exchangeName, "error");
        // 队列2绑定 info  error  warning
        channel.queueBind(queue2Name, exchangeName, "info");
        channel.queueBind(queue2Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "warning");

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

消费者代码:和之前没啥区别,就改一下队列名即可。

2.5 Topics通配符模式

与定向模式不同,路由key可以是一个通配符(.和#)、
.表示可以是一个单词,即只能是一层。
#表示可以是多个单词之间用.隔开,0层或多层。
在这里插入图片描述
生产者代码

public class Producer_Topics {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/itcast");
        factory.setUsername("zsk");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /**
         * 参数: TOPIC("topic"),     :通配符的方式
         */
        String exchangeName = "test_topic";
        // 5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
        // 6.创建队列
        String queue1Name = "test_topic_queue1";
        String queue2Name = "test_topic_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 7.绑定队列和交换机
        // routing key  系统的名称.日志的级别。
        // 需求: 所以error级别的日志存入数据库,所有order系统的日志存入数据库
        channel.queueBind(queue1Name, exchangeName, "#.error");
        channel.queueBind(queue1Name, exchangeName, "order.*");
        // 队列2绑定 info  error  warning
        channel.queueBind(queue2Name, exchangeName, "*.*");


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

3.SpringBoot整合RabbitMQ

依赖 -> yml -> 配置类(交换机、队列、绑定) -> 测试类(注入RabbitTemplete,发送消息)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
	rabbitmq:
		host: localhost   # ip
		ip: 5672
		username: guest
		password: guest
		virtual-host: /

3.1 生产者

@Configuration
public class RabbitMQConfig {

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

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

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

    // 3.队列和交换机绑定关系  Binding
    /**
	*  1.知道哪个队列
	*  2.知道哪个交换机
	*  3.routing key
	*/

    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,@Qualifier("bootExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}
	//测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class ReggieApplicationTest {

    // 1.注入RabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSend() {
    	// 发送的交换机  路由key   消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.haha", "boot mq hello");
    }
}

3.2消费者

定义监听类(使用@RabbitListener注解完成队列监听)

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

4.RabbitMQ高级特性

4.1 消息的可靠性

生产者端:通过确认和回退机制确保投递消息的可靠性。
消费端:通过确认机制确保接收处理消息。

4.1.1 生产者之确认机制

confirm模式:发送者在发送消息时设置confirm回调监听,到达或不到达交换机都会调用。

spring:
  rabbitmq:
    host: localhost   # ip
    ip: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated  # 开启确认模式
	// 配置交换机和队列和binding
	public static final String EXCHANGE_NAME1 = "testExchange";
    public static final String QUEUE_NAME1 = "testQueue";

	@Bean("testExchange")
    public Exchange testExchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME1).durable(true).build();
    }

    @Bean("testQueue")
    public Queue testQueue() {
        return QueueBuilder.durable(QUEUE_NAME1).build();
    }

    @Bean
    public Binding bindQueue2Exchange(@Qualifier("testQueue") Queue queue,@Qualifier("testExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
    }
    /*  测试
        确认模式:
            1.确认模式开启:publisher-confirm-type: correlated
            2.在rabbitTemplate定义ConfirmCallBack回调函数
     */
    @Test
    public void testConfirm() {
        // 2.定义回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * @param correlationData   相关配置信息
             * @param b     exchange交换机 是否成功收到了消息。true 成功, false 失败
             * @param s     失败原因 (交换机名字错了、路由失败等)
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("confirm方法被执行了....");

                if (b) {
                    // 接收成功
                    System.out.println("接收成功消息" + s);
                } else {
                    // 接收失败
                    System.out.println("接收失败原因" + s);
                    // 做一些处理,让消息再次发送
                }
            }
        });
        // 3.发送消息 (上面回调函数被自动执行了...)
        rabbitTemplate.convertAndSend("testExchange", "confirm", "message confirm...");
    }

4.1.2 生产者之退回模式

Exchange路由到Queue失败时 才会执行 ReturnCallBack

spring:
  rabbitmq:
    host: localhost   # ip
    ip: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated  # 开启
    publisher-returns: true #开启
    /**
     * 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack
     * 步骤:
     *  1. 开启回退模式:publisher-returns: true
     *  2. 设置ReturnCallBack
     *  3. 设置Exchange处理消息的模式:
     *      1.如果消息没有路由到Queue,则丢弃消息(默认)
     *      2.如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
     */
    @Test
    public void testReturn() {

        // 设置交换机处理失败消息模式  (设置为true后,会把消息返回给生产者)
        rabbitTemplate.setMandatory(true);

        // 2.设置ReturnCallBack
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             * Message message, int replyCode, String replyText, String exchange, String routingKey
             *  1.message 消息对象
             *  2.replyCode  错误码
             *  3.replyText  错误消息
             *  4.exchange   交换机
             *  5.routingKey 路由键
             * @param returnedMessage  上面信息被包装成对象了
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("return 执行了");

                System.out.println(returnedMessage.getMessage());   // (Body:'message confirm...' ..)
                System.out.println(returnedMessage.getReplyCode()); // 312
                System.out.println(returnedMessage.getReplyText()); // NO_ROUTE
                System.out.println(returnedMessage.getExchange());  // testExchange
                System.out.println(returnedMessage.getRoutingKey());// confirm45

                // 处理
            }
        });

        // 3.发送消息  (这里路由key填个错的)
        rabbitTemplate.convertAndSend("testExchange", "confirm45", "message confirm...");
    }

测试:return 和 confirm 方法可以同时使用

4.1.3 消费者之确认模式

■ 自动确认:acknowledge=“none” – 消费者收到消息会自动给brake发送回执签收
■ 手动确认:acknowledge=“manual” – 消费者可以处理完消息手动调用代码发送签收
■ 根据移除情况确认:acknowledge=“auto” (使用麻烦,不作讲解)

spring:
  rabbitmq:
    host: localhost   # ip
    ip: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      direct:
        acknowledge-mode: manual	# 手动签收
/**
* Consumer ACK机制:
*  1.设置手动签收
*  2.Spring中让监听器类实现ChannelAwareMessageListener接口, SpringBoot 加个channel参数
*  3.如果消息处理成功,则调用 channel 的 basicAck() 签收
*  4.如果消息处理失败,则调用 channel 的 basicNack() 拒绝签收,broker重新发送给consumer
*/
@Component
public class RabbitMQListener {

    @RabbitListener(queues = "testQueue")
    public void ListenerTestQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签

        try {
            // 1.接收转换消息
            System.out.println(new String(message.getBody()));
            // 2.处理业务逻辑
            System.out.println("处理业务逻辑...");
            // int i = 3/0;    // 出错了,broke一直重发
            // 3.手动签收
            channel.basicAck(deliveryTag, true);    // true可以签收多条消息(之前的消息)
        } catch (Exception e) {

            // 4.拒绝签收
            // 第三个参数: requeue 重回队列如果设置为true,则消息重新回到queue ,broker会重新发送该消息给消费端
            channel.basicNack(deliveryTag, true, true);
            // channel.basicReject(deliveryTag, true);  // 单条处理,推荐使用上面的
        }
    }
}

4.1.4 可靠性总结

1.持久化 : exchange要持久化 queue要持久化 message要持久化
2.生产方确认 Confirm
3.消费方确认 Ack
4.Broker高可用

4.2 消费端限流

spring:
  rabbitmq:
    host: localhost   # ip
    ip: 5672
    username: guest
    password: guest
    virtual-host: /
    # publisher-confirm-type: correlated  # 开启
    # publisher-returns: true #开启
    listener:
      direct:
        acknowledge-mode: manual	 # 一定要配置为手动确认
        consumers-per-queue: 1       # 限制消费者一次拉取消息个数
/**
* Consumer限流机制
*  1.确保ack机制为手动确认。
*  2.Spring中listener-container配置属性     boot:   consumers-per-queue: 1
*      prefetch = 1000, 消费者每次从mq拉取1000条消息来消费,直到手动确认消费完毕后,才会继续拉取下一批消息
*/
@Component
public class QoSListener {

    @RabbitListener(queues = "testQueue")
    public void ListenerTestQueue(Message message, Channel channel) throws Exception {
        //1. 获取消息
        System.out.println(new String(message.getBody()));
        //2. 处理业务逻辑
        //3. 签收
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
}

4.3 TTL

设置消息的存活时间,到达指定时间,没有被消费,该消息就会被清除。
如果队列设置了TTL,到达指定时间,就会清除全部消息。
应用场景:
在这里插入图片描述
参数不用记,控制台都有 参数不用记,控制台都有

    @Bean("test_exchange_ttl")
    public Exchange testExchangeTTL() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME2).durable(true).build();
    }

    @Bean("test_queue_ttl")
    public Queue testQueueTTL() {
        // todo: ttl = 100000     队列统一时间过期
        // return QueueBuilder.durable(QUEUE_NAME2).withArgument("x-message-ttl", 10000).build();
        return QueueBuilder.durable(QUEUE_NAME2).ttl(10000).build();
    }

    @Bean
    public Binding bindQueueExchangeTTL(@Qualifier("test_queue_ttl") Queue queue, @Qualifier("test_exchange_ttl") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("ttl.#").noargs();
    }
    /**
     * TTL:过期时间
     *  1.队列统一过期
     *
     *  2.消息单独过期
     *
     *  如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
     *  队列过期后,会将队列所有消息全部移除。
     *  消息过期:只有消息在队列顶端,才会判断其是否过期(移除掉)   -- 否则遍历队列一个个判断性能太差
     */
    @Test
    public void testTtl() {
/*        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hh", "message ttl..." + i);
        }*/

        // 消息的后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 1.设置message的信息
                message.getMessageProperties().setExpiration("5000");   // 消息的过期时间
                // 2.返回该消息
                return message;
            }
        };
        // 消息单独过期
        rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hh", "message ttl...", messagePostProcessor);
    }

4.4 死信队列

死信队列和死信交换机,用于处理过期的消息。
给队列设置参数:x-dead-letter-exchange和x-dead-letter-routing-key
在这里插入图片描述
消息成为死信的三种情况:
○ 1.队列消息长度到达限制;
○ 2.消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
○ 3.原队列存在消息过期设置。消息到达超时时间未被消息;

    /**
     * 死信队列:
     *  1.声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)
     *  2.声明死信队列(queue_dlx)和死信交换机(exchange_dlx)
     *  3.正常队列绑定死信交换机
     *      设置两个参数:
     *          * x-dead-letter-exchange: 死信交换机名称
     *          * x-dead-letter-routing-key: 发送给死信交换机的routingKey
     */

    // 声明正常队列
    @Bean("test_queue_dlx")
    public Queue testQueueTestDlx() {
        // 正常队列绑定死信交换机     这里 key 可以绑定即可 dlx.hehe
        return QueueBuilder.durable("test_queue_dlx")
                .withArgument("x-dead-letter-exchange", "exchange_dlx")
                .withArgument("x-dead-letter-routing-key", "dlx.hehe")
                .ttl(10000)     // 队列过期时间
                .maxLength(10)     // 队列的长度限制
                .build();
    }

    @Bean("test_exchange_dlx")
    public Exchange testExchangeTestDlx() {
        return ExchangeBuilder.topicExchange("test_exchange_dlx").durable(true).build();
    }

    @Bean
    public Binding bindQueueExchangeTestDlx(@Qualifier("test_queue_dlx") Queue queue, @Qualifier("test_exchange_dlx") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("test.dlx.#").noargs();
    }

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

    @Bean("exchange_dlx")
    public Exchange testExchangeDlx() {
        return ExchangeBuilder.topicExchange("exchange_dlx").durable(true).build();
    }

    @Bean
    public Binding bindQueueExchangeDlx(@Qualifier("queue_dlx") Queue queue, @Qualifier("exchange_dlx") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
    }

测试结果需要到管理控制台看

    /**
     * 发送测试死信消息:
     *  1.过期时间
     *  2.长度限制
     *  3.消息拒收
     */
    @Test
    public void testDlx() {
        // 1.测试过期时间,死信消息
        // rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");

        // 2.测试长度限制后,消息死信
/*        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");
        }*/

        // 3.测试消息拒收
        rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");
    }
	// 消费端拒收消息
	@RabbitListener(queues = "test_queue_dlx")
    public void ListenerTestQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签

        try {
            // 1.接收转换消息
            System.out.println(new String(message.getBody()));
            // 2.处理业务逻辑
            System.out.println("处理业务逻辑...");
            int i = 3/0;    // 出错了,broke一直重发
            // 3.手动签收
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            // 4.拒绝签收
            System.out.println("拒绝接收");
            // 第三个参数: requeue    不重回队列
            channel.basicNack(deliveryTag, true, false);
        }
    }

4.5 延迟队列 – 重点

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费
需求:
1.下单后,30分钟未支付,取消订单,回滚库存。
2.新用户注册成功7天后,发送短信问候。
实现方式:
1.定时器 [下单之后存到数据库表中,然后每隔一段时间判断用户是否付款,超时回滚]
2.延迟队列
在RabbitMQ中使用TTL+死信队列组合实现延迟队列的效果。
在这里插入图片描述

    /**
     * 延迟队列:
     *  1.定义正常交换机(order_exchange)和队列(order_queue)
     *  2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
     *  3.绑定,设置正常队列过期时间为30分钟
     */
    // 声明正常队列
    @Bean("order_queue")
    public Queue testQueueOrder() {
        // 正常队列绑定死信交换机
        return QueueBuilder.durable("order_queue")
                .withArgument("x-dead-letter-exchange", "order_exchange_dlx")
                .withArgument("x-dead-letter-routing-key", "dlx.order.cancel")
                .ttl(10000)     // 队列过期时间
                .maxLength(10)     // 队列的长度限制
                .build();
    }

    @Bean("order_exchange")
    public Exchange testExchangeOrder() {
        return ExchangeBuilder.topicExchange("order_exchange").durable(true).build();
    }

    @Bean
    public Binding bindQueueExchangeOrder(@Qualifier("order_queue") Queue queue, @Qualifier("order_exchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("order.#").noargs();
    }

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

    @Bean("order_exchange_dlx")
    public Exchange testExchangeOrderDlx() {
        return ExchangeBuilder.topicExchange("order_exchange_dlx").durable(true).build();
    }

    @Bean
    public Binding bindQueueExchangeOrderDlx(@Qualifier("order_queue_dlx") Queue queue, @Qualifier("order_exchange_dlx") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("dlx.order.#").noargs();
    }
@Component
public class OrderListener {

    @RabbitListener(queues = "order_queue_dlx")     // 死信队列
    public void ListenerTestQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签

        try {
            // 1.接收转换消息
            System.out.println(new String(message.getBody()));
            // 2.处理业务逻辑
            System.out.println("处理业务逻辑...");
            System.out.println("根据订单id查询其状态...");
            System.out.println("判断状态是否为支付成功");
            System.out.println("取消订单,回滚库存...");
            // 3.手动签收
            channel.basicAck(deliveryTag, true);    // true可以签收多条消息(之前的消息)
        } catch (Exception e) {
            // 4.拒绝签收
            System.out.println("拒绝签收");
            channel.basicNack(deliveryTag, true, false);
        }
    }
}
    @Test
    public void testDelay() throws Exception {
        //1.发送订单信息。 将来是在订单系统中,下单成功后,发送消息
        rabbitTemplate.convertAndSend("order_exchange", "order.msg", "订单信息: id=1,time=2023年4月3日19:47:11");
        //2.打印倒计时10s	10s之后死信队列才处理消息
        for (int i = 10; i > 0; i--) {
            System.out.println(i + "...");
            Thread.sleep(1000);
        }
    }

4.6 日志监控 和 消息追踪

之后慢慢了解

5 RabbitMQ应用问题

5.1 消息可靠性

主要通过延迟发送第二次消息,防止第一次消息发送失败。
在这里插入图片描述

5.2 消息幂等性

幂等性:一次和多次请求某一个资源,对于资源本身应该具有同样的结果。
在MQ中,指多个消费者消费相同消息,最后结果相同(如数据库库存等)
通过乐观锁实现,增加一个version字段。
在这里插入图片描述

6 RabbitMQ集群搭建

单机多实例

service rabbitmq-server stop		# 停止服务,因为单机上要通过进程号区分
# 启动第1个实例	rabbit1监听5673 其管理控制台进程号是 15672 默认的
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start		
# 启动第2个实例   其管理控制台进程号是 15674
RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start
# rabbitmqctl -n rabbit1 stop		# 停止对应服务
# rabbitmqctl -n rabbit2 stop

rabbit1设置为主节点

rabbitmqctl -n rabbit1 stop_app 
rabbitmqctl -n rabbit1 reset
rabbitmqctl -n rabbit1 start_app

rabbit2设置为从节点

rabbitmqctl -n rabbit2 stop_app
rabbitmqctl -n rabbit2 reset
rabbitmqctl -n rabbit2 join_cluster rabbit1@'super' ###''内是super主机名换成自己的
rabbitmqctl -n rabbit2 start_app

rabbitmqctl cluster_status -n rabbit1 // 查看集群状态
此时并没有 主从同步,主节点数据没有同步到从节点上
RabbitMQ镜像集群配置
要复制队列内容到集群里的每个节点,必须要创建镜像队列。
在这里插入图片描述
– 这样每个节点消息进行了同步
但是这样存在一个问题,一个节点挂了,访问另一个节点需要修改端口,所以我们可以通过路由解决这个问题
可以通过负载均衡-HAProxy解决该问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值