消息队列之RabbitMQ,边学边忘,不妨复习复习...

RabbitMq

目录:
1.安装
2.基本使用
3.消息确认机制
4.不采用轮巡
5.发送确认
6.交换机
7.信息队列
8.延迟队列
9.延迟队列优化
10.基于插件的延迟队列
11.springboot发送确认和回退消息
12.备份交换机
13.幂等性
14.优先级队列
15.惰性队列
16.RabbitMQ集群
简介:

MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是

message 而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常

见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不

用依赖其他服务。

1. 安装

安装步骤:

1.官方下载网址: https://www.rabbitmq.com/download.html
2.文件上传

上传到/usr/local/software 目录下

3.安装文件

# 安装erlang,rabbitmq需要erlang环境和scoat
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y
# 执行rpm安装
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm

4.常用命令

# 添加开机启动 RabbitMQ 服务
chkconfig rabbitmq-server on
# 启动服务
/sbin/service rabbitmq-server start
# 查看服务状态
/sbin/service rabbitmq-server status
# 停止服务
/sbin/service rabbitmq-server stop
# 开启web管理插件
rabbitmq-plugins enable rabbitmq_management

5.创建web登录用户

# 创建账号
rabbitmqctl add_user admin 123
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"1

6.使用账号密码,访问面板

http://ip地址:15672/

注: 如果采用云服务器搭建,记得开启567215672端口

2. 基本使用

环境搭建省略

引入依赖:

 <!--rabbitmq 依赖客户端-->
 <dependency>
     <groupId>com.rabbitmq</groupId>
     <artifactId>amqp-client</artifactId>
     <version>5.8.0</version>
 </dependency>
 <!--操作文件流的一个依赖-->
 <dependency>
     <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
     <version>2.6</version>
 </dependency>

抽象工具:

创建信道

public class RabbitMqUtils {
     //得到一个连接的 channel
     public static Channel getChannel() throws Exception{
     //创建一个连接工厂
         ConnectionFactory factory = new ConnectionFactory();
         factory.setHost("182.92.234.71");
         factory.setUsername("admin");
         factory.setPassword("123");
         Connection connection = factory.newConnection();
         Channel channel = connection.createChannel();
         return channel;
	 } 
}

创建生产者

public class Producer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("119.91.237.21");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123");

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

        /**
         * 1. 队列名称
         * 2. 是否将队列里面的信息持久化
         * 3. 该队列是否提供给多个消费者使用
         * 4. 在消费者读取后,是否将队列删除
         * 5. 其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        // 定义一个要发送的信息
        String msg = "hello word!!!";

        /**
         * 1. 发送到哪个交换机
         * 2. 发送的队列名称
         * 3. 其他参数
         * 4. 字节信息
         */
        channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
        System.out.println("消息发送成功!");
    }
}

创建消费者

public class Consumer {
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("119.91.237.21");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息...");
        // 成功回调函数
        DeliverCallback deliverCallback = (var1,var2)->{
            String message= new String(var2.getBody());
            System.out.println(message);
        };
        // 取消回调函数
        CancelCallback cancelCallback = (var1)->{
            System.out.println("消息被中断");
        };
        
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

多消费者,就是创建两个消费者连接同一个信道

3. 消息确认机制

为了保证在消息处理时,消费者突然宕机,造成消息没有被处理完成,导致消息丢失,rabbitmq提供了消息确认机制,有逐个确认和批量确认。

  • 逐个确认 ,接受到的消息逐个确认
  • 批量确认,接受到信道中多个消息,一起确认
public class Consumer {
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        /**
         * 1. 队列名
         * 2. 是否自动确认
         * 3. 成功接收回调
         * 4. 取消接收回调
         */
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("C1接收到的消息是:" + new String(message.getBody()));
            try {
                Thread.sleep(30000);
                /**
                 * 1. 是该消息的标记
                 * 2. 是否批量确认,可能造成漏读
                 */
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        System.out.println("C1 等待接收消息.....");
        // 第二参数是不自动确认
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, new CancelCallback() {
            @Override
            public void handle(String s) throws IOException {
                System.out.println("取消了消息接收");
            }
        });
    }
}

说明:

是否自动确认,就在于接受时的第二个参数 channel.basicConsume(QUEUE_NAME, false, deliverCallback, new CancelCallback()) ,设置为true 就是自动确认,设置为false 就是批量确认。消息确认命令 channel.basicAck(message.getEnvelope().getDeliveryTag(),false); 第一个参数是该消息的标签,第二参数表示是否自动确认。

4. 不采用轮巡

可以采用以下两种

  • 第一种是在消费者接受消息前,在管道设置前都设置为1 ,这样话,只要你还没有处理完给你的任务,就会将消息任务分发给其他消费者处理
  • 第二种是每个消费者都设置都能接收消息的预取值,这样的话,分发的任务达到饱和的时候,就会将任务交给其他人处理
// 第一种 
channel.basicQos(1);
// 第二种,表示这个消费者信道最大能接收的消息数,满了就会分配给其他消费者信道
channel.basicQos(2);
5. 发送确认

发送确认机制,是保证信息被发送出去了,防止在发送信息途中,发生错误,导致信息无法正常发送,最终导致消息丢失的事情。

  1. 开启发送确认机制
channel.confirmSelect();
  1. 发送信息
channel.basicPublish("", ACK_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
  1. 确认发送成功
boolean b = channel.waitForConfirms();

发送确认机制有一下三种:

  • 单个确认发布
  • 批量确认发布
  • 异步确认发布

测评:

public class ProducerTest {
    public static final String TEST_QUEUE = "test_queue";
    public static final int TEST_TIMES = 500;
    public static void main(String[] args) throws Exception{
        // 单个确认
        // singleton(); // 花费时间为 : 41183
        // 批量确认
        //multi(); // 花费时间为 : 96
        // 异步确认
        async(); // 花费时间为 : 25
    }

    // 单个确认
    public static void singleton() throws Exception{
        Channel channel = ConnectUtil.getChannel();
        channel.queueDeclare(TEST_QUEUE,true,false,false,null);
        // 开启确认机制
        channel.confirmSelect();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < TEST_TIMES; i++) {
            String msg = i + "";
            channel.basicPublish("",TEST_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
            channel.waitForConfirms();
            System.out.println(i + "发送成功");
        }
        long end = System.currentTimeMillis();
        System.out.println("花费时间为 : " + (end - begin)  + "ms");
    }
    // 批量确认
    public static void multi() throws Exception{
        Channel channel = ConnectUtil.getChannel();
        channel.queueDeclare(TEST_QUEUE,true,false,false,null);
        // 开启确认机制
        channel.confirmSelect();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < TEST_TIMES; i++) {
            String msg = i + "";
            channel.basicPublish("",TEST_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
            if (i % TEST_TIMES == 0){
                channel.waitForConfirms();
                System.out.println(i + "发送成功");
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("花费时间为 : " + (end - begin)  + "ms");
    }
    // 异步确认
    public static void async() throws Exception{
        Channel channel = ConnectUtil.getChannel();
        channel.queueDeclare(TEST_QUEUE,true,false,false,null);

        // 开启确认机制
        channel.confirmSelect();
        // 采用
        ConcurrentSkipListMap<Long,String> concurrentSkipListMap = new ConcurrentSkipListMap<>();
        // 添加异步确认处理监听器

        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                System.out.println(l + " 发送成功");
                concurrentSkipListMap.remove(l);
            }

            @Override
            public void handleNack(long l, boolean b) throws IOException {
                System.out.println(l + " 发送失败");
                System.out.println("发送失败的消息" + concurrentSkipListMap);
            }
        });
        long begin = System.currentTimeMillis();
        for (int i = 0; i < TEST_TIMES; i++) {
            String msg = i + "";
            channel.basicPublish("",TEST_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
            // 记录有的信息
            concurrentSkipListMap.put(channel.getNextPublishSeqNo(),msg);
        }
        long end = System.currentTimeMillis();
        System.out.println("花费时间为 : " + (end - begin) + "ms");
    }
}
6. 交换机(Exchanges)

交换机实际上一直都存在,只是我们使用的是默认的交换机,每次发送到哪个交换机,我们填写的都是“” 所以,我们一直使用的是默认的交换机。交换机实际上是来调节消息发送给特定的队列,以及发送的信息可以被多个队列接收,而消费者从不同的队列中读取不同的信息。直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)

交换机分为以下四类:

  • 扇出(fanout): 将一个信息转发给所有链接队列的消费者
  • 直接(direct): 采用routekey实现,生产者生产的某个消息,只发送给特定的消费者
  • 标题(headers):
  • 主题(topic): 还是采用routekey实现,采用特定的语法,实现群发和特定转发。

扇出

消费者声明队列,并且给队列绑定交换机

public class ExReceive01 {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 声明一个临时队列
        String queueName = channel.queueDeclare().getQueue();
        // 声明一个扇出交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        /**
         * 队列绑定交换机
         * 1. 队列名称
         * 2. 交换机名称
         * 3. routerKey 扇出 直接是 ““
         */
        channel.queueBind(queueName,EXCHANGE_NAME,"");
        System.out.println("等待接收信息");
        // 接收信息
        DeliverCallback deliverCallback = (consumerTag,messgae)->{
            System.out.println(new String(messgae.getBody(),"UTF-8"));
        };
        channel.basicConsume(queueName,deliverCallback,msg->{});
    }
}

生产者,给交换机发送信息,并且指明routerkey,fanout的routekey就是 “”

public class ExProducer {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 这里设置第二个参数是true,表示队列持久化
        channel.confirmSelect();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            /**
             * 1. 使用哪个交换机
             * 2. 队列名字
             * 3. 其他参数
             * 4. 字节消息体
             */
            String message = scanner.next();
            System.out.println("生产者发送了消息:" + message);
            // 发送消息,第一个参数是交换机,第二个是routerKey
            channel.basicPublish(EXCHANGE_NAME,"", null, message.getBytes(StandardCharsets.UTF_8));;
        }
    }
}

直接:

消费者,声明临时队列,绑定交换机以及routekey

public class ExReceive01 {
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 声明一个临时队列
        String queueName = channel.queueDeclare().getQueue();
        // 声明一个扇出交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"direct");
        /**
         * 队列绑定交换机
         * 1. 队列名称
         * 2. 交换机名称
         * 3. routerKey 扇出 直接是 ““
         */
        // 绑定routeKey,只接收交换机转发到与routeKey绑定的队列信息
        channel.queueBind(queueName,EXCHANGE_NAME,"orange");
        System.out.println("等待接收信息");
        // 接收信息
        DeliverCallback deliverCallback = (consumerTag,messgae)->{
            System.out.println(new String(messgae.getBody(),"UTF-8"));
        };
        channel.basicConsume(queueName,deliverCallback,msg->{});
    }
}

生产者,生成消息,根据routekey给交换机发送消息

public class ExProducer {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 这里设置第二个参数是true,表示队列持久化
        channel.confirmSelect();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            /**
             * 1. 使用哪个交换机
             * 2. 队列名字
             * 3. 其他参数
             * 4. 字节消息体
             */
            String message = scanner.next();
            System.out.println("生产者发送了消息:" + message);
            // 发送消息,第一个参数是交换机,第二个是routerKey
            channel.basicPublish(EXCHANGE_NAME,"green", null, message.getBytes(StandardCharsets.UTF_8));;
        }
    }
}

主题:

主题模式集合了前两种模式,在即满足向多个消费者队列发送信息,又有直接想某个队列发送信息,采用一种特殊的routeKey。

routerKey规范

发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单
词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", "nyse.vmw", 
"quick.orange.rabbit".这种类型的。当然这个单词列表最多不能超过 255 个字节。	
在这个规则列表中,其中有两个替换符是大家需要注意的
    *(星号)可以代替一个单词
    #(井号)可以替代零个或多个单词

代码如下:

消费者,声明发送的交换机,并且声明需要转发队列的routeKey

public class ExProducer {
    private static final String EXCHANGE_NAME = "direct_logs";
    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 这里设置第二个参数是true,表示队列持久化
        channel.confirmSelect();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            /**
             * 1. 使用哪个交换机
             * 2. 队列名字
             * 3. 其他参数
             * 4. 字节消息体
             */
            String message = scanner.next();
            System.out.println("生产者发送了消息:" + message);
            // 发送消息,第一个参数是交换机,第二个是routerKey
            channel.basicPublish(EXCHANGE_NAME,"green", null, message.getBytes(StandardCharsets.UTF_8));;
        }
    }
}

消费者,指定绑定的交换机以及routeKey,接收消息

public class ExReceive02 {
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();

        // 声明一个扇出交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"direct");
        /**
         * 队列绑定交换机
         * 1. 队列名称
         * 2. 交换机名称
         * 3. routerKey 扇出 直接是 ““
         */
        // 声明一个临时队列
        String queueName = channel.queueDeclare().getQueue();
        // 队列绑定交换机
        channel.queueBind(queueName,EXCHANGE_NAME,"black");
        channel.queueBind(queueName,EXCHANGE_NAME,"green");
        System.out.println("等待接收信息");
        // 接收信息
        DeliverCallback deliverCallback = (consumerTag,messgae)->{
            System.out.println(new String(messgae.getBody(),"UTF-8"));
        };
        channel.basicConsume(queueName,deliverCallback,msg->{});
    }
}

说明:

主题(topic)模式,是前两种的结合,即可以群发又可以单独发

7. 死信队列

概念:

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理

解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息

进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有

后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息

消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时

间未支付时自动失效

死信的来源:

1. 消息TTL过期
2. 队列长度到达最大值
3. 消息被拒绝(basic reject 或 basic.nack)并且 requeue = false

架构图:
在这里插入图片描述

  1. 消息TTL过期

生产者,设置到期时间,时间一到就会加入死信队列

public class Producer {
    // 普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        AMQP.BasicProperties properties = new AMQP.BasicProperties()
                .builder().expiration("10000").build();
        for (int i = 1; i < 11; i++) {
            String info = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,info.getBytes());
            System.out.println(info + " 发送成功!");
        }
    }
}

消费者,声明普通队列、交换机和死信队列、交换机,分别进行交换机绑定队列,并且在声明普通队列的时候,告诉普通队列,自己的死信队列的交换机名字,以及自己交换机死信队列的的routing-key

public class Consumer01 {
    // 普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    // 死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange";
    // 普通队列
    public static final String NORMAL_QUEUE = "normal_queue";
    // 死信队列
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        // 声明两个交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        // 声明队列参数
        Map<String,Object> arguments = new HashMap<>();
        // 声明死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        // 声明私信交换机的routeKey
        arguments.put("x-dead-letter-routing-key","anyi");
        // 声明两个队列,并且告诉普通队列它的死信队列是谁
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 两个队列分别绑定各自的交换机
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"anyi");

        System.out.println("C1等待接受消息!");
        DeliverCallback deliverCallback = (msgTag, msg)->{
            String message = new String(msg.getBody(),"UTF-8");
            System.out.println("C1接收到消息: " +message);
        };
        // 消费信息
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,msg->{});
    }
}

死信队列消费者,和普通没有区别

public class Consumer02 {
    // 死信队列
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception{
        Channel channel = ConnectUtil.getChannel();
        System.out.println("C2等待接受消息!");
        DeliverCallback deliverCallback = (msgTag, msg)->{
            String message = new String(msg.getBody(),"UTF-8");
            System.out.println("C2接收到消息: " +message);
        };
        // 消费信息
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,msg->{});
    }

}
  1. 队列长度到达最大值

在消费者声明队列处,参数中添加最大队列长度,当超过六条就会加入到死信队列

arguments.put("x-max-length",6);
  1. 消息被拒绝(basic reject 或 basic.nack)并且 requeue = false

普通队列拒绝处理的消息,会被加入到死信队列中,修改接收消息的逻辑

DeliverCallback deliverCallback = (msgTag, msg)->{
    String message = new String(msg.getBody(),"UTF-8");
    if (message.equals("info5")){
        System.out.println("消息info5被拒绝处理");
        /**
                 * 1. 拒绝消息的id标识
                 * 2. 是否重新加入队列
                 */
        channel.basicReject(msg.getEnvelope().getDeliveryTag(),false);
    }else {
        System.out.println("C1就收到了消息:" + message);
        /**
                 * 1. 拒绝消息的id标识
                 * 2. 是否批量确认
                 */
        channel.basicAck(msg.getEnvelope().getDeliveryTag(),false);
    }
};
8. 延迟队列

如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

代码架构图 :

创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

在这里插入图片描述

代码实现:

配置架构类

@Configuration
public class TtlQueueConfig {
    public static final String X_EXCHANGE = "X";
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    public static final String DEAD_LETTER_QUEUE = "QD";
    // 交换机
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new  DirectExchange(X_EXCHANGE);
    }
    // 死信交换机
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new  DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }
    // 队列A 并且绑定死信交换机和过期时间
    @Bean("QA")
    public Queue queueA(){
        return QueueBuilder.durable(QUEUE_A)
                .deadLetterExchange(Y_DEAD_LETTER_EXCHANGE)
                .ttl(10000)
                .deadLetterRoutingKey("YD")
                .build();
    }
    // 队列B 绑定死信交换机和过期时间
    @Bean("QB")
    public Queue queueB(){
        return QueueBuilder.durable(QUEUE_B)
                .deadLetterExchange(Y_DEAD_LETTER_EXCHANGE)
                .ttl(40000)
                .deadLetterRoutingKey("YD")
                .build();
    }
    // 死信队列
    @Bean("QD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE)
                .build();
    }
    // 将队列A绑定到交换机X
    @Bean
    public Binding queueABindX(@Qualifier("QA")Queue queueA, @Qualifier("xExchange")DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }
    // 将队列B绑定到交换机X
    @Bean
    public Binding queueBBindX(@Qualifier("QB")Queue queueB, @Qualifier("xExchange")DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }
    // 将死信队列绑定到死信交换机
    @Bean
    public Binding queueDBindY(@Qualifier("QD")Queue queueD, @Qualifier("yExchange")DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

接收消息,并且存入延迟队列的controller

@RequestMapping("ttl")
@RestController
@Slf4j
public class SendMsgController {
    @Autowired
    private  RabbitTemplate rabbitTemplate;
    @GetMapping("sendMsg/{msg}")
    public void sendMsg(@PathVariable("msg") String msg){
        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), msg);
        rabbitTemplate.convertAndSend("X","XA","来自10s队列的消息:"+msg);
        rabbitTemplate.convertAndSend("X","XB","来自40s队列的消息:"+msg);
    }
    @GetMapping("test")
    public void test(){
        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date());
    }
}

rabbitmq监听队列类

@Component
@Slf4j
public class MsgConsumer {
    @RabbitListener(queues = "QD")
    public void receiveMsg(Message message, Channel channel){
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info("接收到一条延迟独立的消息为 : " + msg);
    }
}

说明:

集成springboot集成rabbitmq消息队列中的延迟队列的简单实现,所有参数设置及队列、交换机、绑定都有对应的api来调用,极大的提高了开发效率。

9. 延迟队列优化

上面的方法存在一个明显的缺陷,就是消息的过期时间是由队列来设置的,如果需要不同过期时间的队列,我们就需要定义多个消息队列,所以我们对它进行优化,过期时间不再由队列自己来设置,由发送消息的生产者来设置,这样的话,就可以发送不同延迟时间的消息。

修改后的生产者代码

    @GetMapping("/sendMsg/{msg}/{ttl}")
    public void sendMsgByTtl(@PathVariable("msg")String msg,@PathVariable("ttl") String ttl){
        rabbitTemplate.convertAndSend("X","XC",msg,
                correlation -> {
                    // 设置延迟时间
                   correlation.getMessageProperties().setExpiration(ttl);
                    return correlation;
                });
        log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttl, msg);
    }
10. 基于插件的延迟队列

上述的延迟队列仍存在一个问题,如果同时有两个消息进来,先进来的是延迟20秒的,后进来的是延迟2秒的,那么延迟两秒的也只能等延迟20秒的结束了,才能被处理。这样就没有达到真正的发过来的请求延迟多少秒,消息就延迟多少秒,所有我们可以使用插件来完成。

安装插件:

下载:https://www.rabbitmq.com/community-plugins.html/

将文件放到:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

执行安装命令: rabbitmq-plugins enable rabbitmq_delayed_message_exchange

延迟队列配置类

@Configuration
public class DelayQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    @Bean
    public CustomExchange delayExchange(){
        Map<String ,Object> arguments= new HashMap<>();
        arguments.put("x-delayed-type","direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
    }
    @Bean
    public Queue delayQueue(){
        return QueueBuilder.durable(DELAYED_QUEUE_NAME).build();
    }
    @Bean
    public Binding bindingDelayQueueToDelayExchange(Queue delayQueue, CustomExchange delayExchange){
        return BindingBuilder.bind(delayQueue).to(delayExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

生产者类,发送消息,并且设置过期时间

@GetMapping("/sendDelayMsg/{msg}/{delayTime}")
public void sendDelayMsg(@PathVariable("msg")String msg,@PathVariable("delayTime") Integer delayTime){
    rabbitTemplate.convertAndSend(DelayQueueConfig.DELAYED_EXCHANGE_NAME,
                                  DelayQueueConfig.DELAYED_ROUTING_KEY,
                                  msg,
                                  correlation -> {
                                      correlation.getMessageProperties().setDelay(delayTime);
                                      return correlation;
                                  });
    log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给延迟 :{}", new Date(),delayTime, msg);
}
总结

​ 延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

​ 当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景s

11. springboot发布确认和回退消息

简单来说就是在springboot使用rabbitmq消息队列,在发出消息后,在发送出信息后,rabbitmq队列的交换机宕机或其他原因没有接收到消息,或是接收到了消息,由于队列宕机或是队列不存在,导致消息丢失,所以就有了springboot发布确认机制。

架构图:
在这里插入图片描述

主要实现交换机发布确认,以及队列未接收到消息触发消息退回

实现步骤:

  1. 修改配置文件

spring.rabbitmq.publisher-confirm-type=correlated (表示发送消息后自动触回调确认机制)

  1. 添加配置类
@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME,true,false);
    }
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    @Bean
    public Binding bindingQueueToExchange(@PathVariable("confirmQueue")Queue confirmQueue,@PathVariable("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
    }
}
  1. 生产者
@GetMapping("sendMessage/{message}")
public void confirm(@PathVariable("message")String message){
    CorrelationData correlationData = new CorrelationData();
    correlationData.setId("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"123" ,"key1",message,correlationData);
    log.info("confirm发送了一条消息:" + message);


    CorrelationData correlationData1 = new CorrelationData();
    correlationData1.setId("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME ,"key2",message,correlationData1);
    log.info("confirm发送了一条消息:" + message);
}
  1. 回调类

声明回调类,实现RabbitTemplate.ConfirmCallback接口,以及回退消息RabbitTemplate.ReturnCallback接口,并且开始消息回退机制

@Component
@Slf4j
public class MyCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 将当前类注入到RabbitTemplate
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        // 打开回退消息
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(this);
    }
    /**
     * 回调函数
     * @param correlationData 自定义信息
     * @param ack 是否成功接收
     * @param cause 错误原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack){
            log.info("confirm 交换机成功接收消息:id 为 " + correlationData.getId());
        }else {
            log.error("confirm 交换机接收信息失败 错误原因为:" + cause);
        }
    }
    /**
     * @param returned 交换机接收到了,但是队列没有接收到返回的信息 包含交换机名称,routeKey,id等
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
                new String(returned.getMessage().getBody()),returned.getReplyText()
                , returned.getExchange(), returned.getRoutingKey());
    }
}
  1. 消费者
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveConfirm(Message message){
    log.info(ConfirmConfig.CONFIRM_QUEUE_NAME + "队列接收到消息:" + new String(message.getBody()));
}
12. 备份交换机

备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码架构图:
在这里插入图片描述

修改配置类

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    // 备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    // 备份队列
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    // 错误队列
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    @Bean("backupExchange")
    public FanoutExchange confirmBackupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME,true,false);
    }

    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        // 将不可路由的信息,交给backupExchange
        ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                // 持久化
                .durable(true)
                // 为确认交换机设置备份交换机
                .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return (DirectExchange) exchangeBuilder.build();
    }
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    @Bean("backupQueue")
    public Queue backupQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }
    @Bean("warningQueue")
    public Queue waringQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }
    @Bean
    public Binding bindingBackupQueueToExchange(@PathVariable("backupQueue")Queue backupQueue,@PathVariable("backupExchange")FanoutExchange backupExchange){
        return BindingBuilder.bind(backupQueue).to(backupExchange);
    }
    @Bean
    public Binding bindingWarningQueueToExchange(@PathVariable("warningQueue")Queue warningQueue,@PathVariable("backupExchange")FanoutExchange backupExchange){
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }

    @Bean
    public Binding bindingBackupQueueExchange(@PathVariable("confirmQueue")Queue confirmQueue,@PathVariable("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
    }
}

重要代码:

ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
    // 持久化
    .durable(true)
    // 为确认交换机设置备份交换机
    .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高

13. 幂等性

概念:

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

消息重复消费:

消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

14. 优先级队列

有海量订单进入队列,但是队列中有些订单需要被优先处理,有些不着急的订单可以放在后面处理,这个时候,我们就需要使用到优先级队列。

使用步骤:

  1. 控制台页面添加

在这里插入图片描述

  1. 在创建队列的时候添加优先级
Map<String, Object> params = new HashMap();
// 设置优先级范围 0-10
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
  1. 生产者发送消息的时候设定优先级
// 表示消息的优先级是5
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
15. 惰性队列

使用场景:

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

16.RabbitMQ集群

在高并发的情况下,大量的消息同时进入rabbitmq队列,可能造成rabbitmq队列宕机、内存崩溃、主机故障等,所以我们需要采用集群来实现rabbitmq的高可用性。

搭建步骤:

  1. 修改 3 台机器的主机名称
vim /etc/hostname
  1. 配置各个节点的 hosts 文件,让各个节点都能互相识别对方
vim /etc/hosts
# 添加以下内容
10.211.55.74 node1
10.211.55.75 node2
10.211.55.76 node3
  1. 以确保各个节点的 cookie 文件使用的是同一个值
# 在 node1 上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
  1. 启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以下命令)
rabbitmq-server -detached
  1. 在节点2上执行
rabbitmqctl stop_app
(rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app(只启动应用服务)
  1. 在节点 3 执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
  1. 查看集群状态
rabbitmqctl cluster_status
  1. 需要重新设置用户
# 创建账号
rabbitmqctl add_user admin 123
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
  1. 如果需要也可以接触链接
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)
vim /etc/hosts
# 添加以下内容
10.211.55.74 node1
10.211.55.75 node2
10.211.55.76 node3
  1. 以确保各个节点的 cookie 文件使用的是同一个值
# 在 node1 上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
  1. 启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以下命令)
rabbitmq-server -detached
  1. 在节点2上执行
rabbitmqctl stop_app
(rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app(只启动应用服务)
  1. 在节点 3 执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
  1. 查看集群状态
rabbitmqctl cluster_status
  1. 需要重新设置用户
# 创建账号
rabbitmqctl add_user admin 123
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
  1. 如果需要也可以接触链接
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值