消息中间件-----RabbitMQ

中间件

我国企业从20世纪80年代开始就逐渐进行信息化建设,由于方法和体系的不成熟,以及企业业务和市场需求的不断变化,一个企业可能同时运行着多个不同的业务系统,这些系统可能基于不同的操作系统、不同的数据库、异构的网络环境。现在的问题是,如何把这些信息系统结合成一个有机地协同工作的整体,真正实现企业跨平台、分布式应用。中间件便是解决之道,它用自己的复杂换取了企业应用的简单。

中间件(Middleware)是处于操作系统和应用程序之间的软件,也有人认为它应该属于操作系统中的一部分。人们在使用中间件时,往往是一组中间件集成在一起,构成一个平台(包括开发平台和运行平台),但在这组中间件中必须要有一个通信中间件,即中间件=平台+通信,这个定义也限定了只有用于分布式系统中才能称为中间件,同时还可以把它与支撑软件和实用软件区分开来。

1. 什么是MQ

MQ(message queue),从字面意思上看,本质是个队列,先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。
在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。

1.1 MQ特点

1.1.1.流量消峰

举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

1.1.2.应用解耦

以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

1.1.3.异步处理

有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询。或者A提供一个callback api,B执行完之后调用api通知A服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,也不用提供callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。

2. RabbitMQ

2.1.概念

RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。

2.2.四大核心概念

生产者

  • 产生数据发送消息的程序是生产者

交换机

  • 交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。

队列

  • 队列是RabbitMQ内部使用的一种数据结构,尽管消息流经RabbitMQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。

消费者

  • 消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

2.3 安装rabbitmq

使用Docker安装

简单点说,镜像就类似操作系统光盘介质,容器相当于通过光盘安装后的系统。通过光盘(镜像),我们能在不同机器上部署系统(容器),系统内的操作只会保留在当前的系统(容器)中,如果要升级系统,需要使用到光盘,但是可能会导致操作系统的数据丢失。

# 安装最新版的rabitmq
docker pull rabbitmq

# 启动rabbitmq
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq

# 查看运行的容器
docker ps

# 进入容器内部
docker exec -it rabbit /bin/bash

# 开启web界面管理插件
rabbitmq-plugins enable rabbitmq_management

# 添加新用户,创建账号和密码
rabbitmqctl add_user admin xxxxx

# 设置用户角色
rabbitmqctl set_user_tags admin administrator

# 设置用户角色(赋予所有权限,读写)--设置角色后可以不用在赋予权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

# 查看当前用户和角色
rabbitmqctl list_users

# 修改账号密码
rabbitmqctl change_password  admin  123456789

# 查看当前有哪些用户
rabbitmqctl list_users

# 关闭应用
rabbitmqctl stop_app

# 清除应用
rabbitmqctl reset

# 重启应用
rabbitmqctl start_app

用添加的新用户登录web界面xxxxxxxxxxx:15672

docker出现500错误

# 进入容器
docker exec -it rabbit /bin/bash
# 进入配置文件夹
cd /etc/rabbitmq/conf.d/
# 执行指令
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
# 退出容器
exit
# 重启容器
docker restart rabbit

2.4 rabbitmq入门

创建一个空项目,新建两个模块,一个代表生产者应用,一个代表消费者应用。

注:

  • 使用web界面是15672端口。
  • 使用java客户端远程连接是5672端口。所以需要关闭防火墙或开启这两个端口。

消息生产者

public class Producer {
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] args) throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("0.0.0.0");
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setPort(5672);
        //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        try(Connection connection = factory.newConnection(); Channel channel =
                connection.createChannel()) {
            /**
             * 生成一个队列
             * 1.队列名称
             * 2.队列里面的消息是否持久化 默认消息存储在内存中
             * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
             * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
             * 5.其他参数
             */
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            String message="hello world";
            /**
             * 发送一个消息
             * 1.发送到哪个交换机
             * 2.路由的 key 是哪个
             * 3.其他的参数信息
             * 4.发送消息的消息体
             */
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("消息发送完毕");
        }
    }
}

启动该服务,在web端查看

  • 该消息已经被生产者生产,且已经放入rabbitmq中。

消息消费者

public class Consumer {
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("0.0.0.0");
        factory.setUsername("admin");
        factory.setPassword("admin");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息....");
        //推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback=(consumerTag, delivery)->{
            String message= new String(delivery.getBody());
            System.out.println("消费者消费消息:"+message);
        };
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println("消息消费被中断");
        };
        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

3. RabbitMq支持消息的模式

  • 简单模式
    • 对应交换机类型:默认交换机类型 direct
  • 工作队列模式
    • 对应交换机类型:默认交换机类型 direct
  • 发布订阅模式
    • 对应交换机类型:fanout
    • 特点:Fanout—发布与订阅模式,是一种广播机制,需要绑定关系,它是没有路由key的模式。
  • 路由模式
    • 对应交换机类型:direct
    • 特点:有routing-key的匹配模式
  • 主题Topic模式
    • 对应交换机类型:topic
    • 特点:模糊的routing-key的匹配模式
  • 参数模式
    • 类型:headers
    • 特点:参数匹配模式

默认交换机类型

3.1 发布订阅模式---fanout

通过原始的方式整合spring(后续会整合springboot,采用配置类方式。)

<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>

3.1.1 生产者

  • 前提:在web端新建fanout_exchange交换机,和需要广播的队列建立绑定关系。和该交换机建立绑定关系的所有队列都能收到消息。
public class Producer {
   
    public static void main(String[] args) {
        //1.新建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2.设置rabbitmq连接参数
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        //3.设置根路径
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            //4.创建连接,并为该连接起一个名字
            connection = factory.newConnection("生产者");
            //5.根据连接创建一个信道
            channel = connection.createChannel();
            //6.发送消息内容--String
            String message = "Hello fanout!";
            //7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
            String exchangeName = "fanout_exchange";
            //8.设置路由key---fanout模式无路由key
            String routingKey = "";
            //9.发送  参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
            System.out.println("消息发送成功。");

        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("消息发送出现异常。");
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }if (connection!=null &&connection.isOpen()){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

启动该生产者,web端查看

由于fanout_exchange只和queue1和queue3进行了绑定,所以只有这两个队列收到了广播消息。

3.1.2 消费者

  • 通过实现runnable接口,重写run方法实现多线程开启。
  • 前提:通过web界面创建了队列(也可以通过代码创建队列)
public class Consumer  implements Runnable{
    @Override
    public void run() {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;
        //将线程名作为队列名,创建线程的时候传入队列名即可。
       final String queueName = Thread.currentThread().getName();

        try {
            connection=factory.newConnection("消费者");
            channel=connection.createChannel();

              /**
             * 创建队列方法-----Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             * 1. queue 队列的名字
             * 2. durable 队列是否持久化
             * 3. exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             * 4. autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息
             * 5. arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             */
            //channel.queueDeclare(queueName,true,false,false,null);

            //接收消息
            channel.basicConsume(queueName, true, new DeliverCallback() {
                //接收消息成功回调函数
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(),"utf-8"));
                }
            }, new CancelCallback() {
                //接收消息异常回调函数
                @Override
                public void handle(String s) throws IOException {
                    System.out.println("消息接收失败");
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Consumer(),"queue1").start();
        new Thread(new Consumer(),"queue2").start();
        new Thread(new Consumer(),"queue3").start();
    }
}

只有1和3线程消费了消息,因为线程1和线程3分别绑定的是队列1和队列3。

3.2 路由模式---direct

  • 交换机类型:direct
  • 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。

3.2.1 生产者

public class Producer {

    //前提:在web端新建direct_exchange交换机,和需要广播的队列建立绑定关系和路由key。
    //和该交换机建立绑定关系且符合路由key的所有队列都能收到消息。
    public static void main(String[] args) {
        //1.新建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2.设置rabbitmq连接参数
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        //3.设置根路径
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            //4.创建连接,并为该连接起一个名字
            connection = factory.newConnection("生产者");
            //5.根据连接创建一个信道
            channel = connection.createChannel();
            //6.发送消息内容--String
            String message = "Hello direct!";
            //7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
            String exchangeName = "direct_exchange";

            //8.设置路由key---direct模式有路由key----根据路由key,队列1和2能收到数据
            String routingKey = "email";
            //String routingKey2 = "sms";

            //9.发送  参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());

             //可以根据不同的路由key发送给多个队列
            //channel.basicPublish(exchangeName, routingKey2, null, message.getBytes());

            System.out.println("消息发送成功。");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("消息发送出现异常。");
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }if (connection!=null &&connection.isOpen()){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.2.2 消费者

消费者和fanout代码相同

public class Consumer  implements Runnable{
    @Override
    public void run() {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;
        //将线程名作为队列名,创建线程的时候传入队列名即可。
       final String queueName = Thread.currentThread().getName();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            connection=factory.newConnection("消费者");
            channel=connection.createChannel();
            channel.basicConsume(queueName, true, new DeliverCallback() {
                //接收消息成功回调函数
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(),"UTF-8"));
                }
            }, new CancelCallback() {
                //接收消息异常回调函数
                @Override
                public void handle(String s) throws IOException {
                    System.out.println("消息接收失败");
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Consumer(),"queue1").start();
        new Thread(new Consumer(),"queue2").start();
        new Thread(new Consumer(),"queue3").start();
    }
}

3.3 主题模式---topic

  • 类型:topic
  • 特点:Topic模式是direct模式上的一种叠加,增加了模糊路由RoutingKey的模式。

3.3.1 生产者

在web端建立交换机和队列之间的绑定关系,及每个队列的模糊匹配规则。

public class Producer {

    //前提:在web端新建topic_exchange交换机,和需要广播的队列建立绑定关系和路由key。
    //和该交换机建立绑定关系且符合路由key的所有队列都能收到消息。
    public static void main(String[] args) {
        //1.新建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2.设置rabbitmq连接参数
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        //3.设置根路径
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            //4.创建连接,并为该连接起一个名字
            connection = factory.newConnection("生产者");
            //5.根据连接创建一个信道
            channel = connection.createChannel();
            //6.发送消息内容--String
            String message = "Hello topic!";
            //7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
            String exchangeName = "topic_exchange";
            //8.设置路由key---topic模式有模糊路由匹配规则
            String routingKey = "com.email.phone.xxx";
            //9.发送  参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
            //可以根据不同的key发送给多个队列
            //channel.basicPublish(exchangeName, routingKey, null, message.getBytes());

            System.out.println("消息发送成功。");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("消息发送出现异常。");
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }if (connection!=null &&connection.isOpen()){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.3.2 消费者

消费者和之前的一样。

public class Consumer  implements Runnable{
    @Override
    public void run() {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;
        //将线程名作为队列名,创建线程的时候传入队列名即可。
       final String queueName = Thread.currentThread().getName();
        try {
            connection=factory.newConnection("消费者");
            channel=connection.createChannel();
            //接收消息成功回调函数
            //接收消息异常回调函数
            channel.basicConsume(queueName, true,
                    (s, delivery) -> System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8")),
                    s -> System.out.println("消息接收失败"));
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Consumer(),"queue1").start();
        new Thread(new Consumer(),"queue2").start();
        new Thread(new Consumer(),"queue3").start();
    }
}

3.4 工作模式---轮询

1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;

3.4.1 轮询模式生产者

生产者将数据放入一个指定的队列,由两个消费者共同消费该队列中的数据。

public class Producer {

    public static void main(String[] args) {

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;

        try {
            connection = factory.newConnection("轮询生产者");
            channel = connection.createChannel();
            //创建一个队列queue5
            channel.queueDeclare("queue5",true,false,false,null);

            //发送消息,轮询使用默认交换机即可
            for (int i = 0; i < 20; i++) {
                String message="work: "+i;
                channel.basicPublish("","queue5",null,message.getBytes());
            }
            
            System.out.println("消息发送成功。");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("消息发送出现异常。");
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }if (connection!=null &&connection.isOpen()){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

此时队列5中就存在20条数据了。

3.4.2 轮询模式消费者

消费者1

该消费者消费速度慢,通过线程休眠模拟速度。

public class Consumer1 {
    public static void main(String[] args) {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;


        try {
            connection=factory.newConnection("消费者");
            channel=connection.createChannel();
            channel.basicConsume("queue5", true,
                    (s, delivery) -> {
                        System.out.println("消费者1消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    },
                    s -> System.out.println("消息接收失败"));
            System.out.println("work1开始接收消息");
            //加入该代码,回调函数才执行
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者2

public class Consumer2 {
    public static void main(String[] args) {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;

        try {
            connection=factory.newConnection("消费者");
            channel=connection.createChannel();

            channel.basicConsume("queue5", true,
                    (s, delivery) -> {
                        System.out.println("消费者2消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    },
                    s -> System.out.println("消息接收失败"));
            System.out.println("work2开始接收消息");
            //加上该代码,方法回调才生效。
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试,先启动消费者,再启动生产者

虽然消费者2比消费者1执行速度快,但总体消费数据条数还是一样的。


3.5 工作模式入门案例---公平

  • 特点:由于消息接收者处理消息的能力不同,存在处理快慢的问题,我们就需要能者多劳,处理快的多处理,处理慢的少处理;

3.5.1 公平模式生产者

先启动一次生产者,在虚拟根节点下创建queue6队列。

public class Producer {

    public static void main(String[] args) {

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;

        try {
            connection = factory.newConnection("公平生产者");
            channel = connection.createChannel();
            //创建一个队列
            channel.queueDeclare("queue6",true,false,false,null);

            //发送消息,轮询使用默认交换机即可
            for (int i = 1; i <= 20; i++) {
                String message="work: "+i;
                channel.basicPublish("","queue6",null,message.getBytes());
            }
            System.out.println("消息发送成功。");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("消息发送出现异常。");
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                } 
            }if (connection!=null &&connection.isOpen()){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.5.2 公平模式消费者

  • 将自动应答改为false。
  • 设置同一时刻服务器发多少条消息给消费者。channel.basicQos(1);
  • 在回调方法中手动应答。

消费者1

public class Consumer1 {
    public static void main(String[] args) {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;
        try {
            connection=factory.newConnection("公平消费者1");
            channel=connection.createChannel();
            //1.同一时刻,服务器只会推送一条消息给消费者
            channel.basicQos(1);
            Channel finalChannel = channel;
             //2.第二个参数改为手动应答
            channel.basicConsume("queue6", false,
                    (s, delivery) -> {
                        try {
                            System.out.println("公平消费者1消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(2000);
                            //3.手动应答
                            finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    },
                    s -> System.out.println("消息接收失败"));
            System.out.println("work1开始接收消息");
            //加入该代码,回调函数才执行
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者2

public class Consumer2 {
    public static void main(String[] args) {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("8.141.52.45");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("123");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel=null;

        try {
            connection=factory.newConnection("公平消费者2");
            channel=connection.createChannel();

            channel.basicQos(1);

            Channel finalChannel = channel;
            channel.basicConsume("queue6", false,
                    (s, delivery) -> {
                        try {
                            System.out.println("公平消费者2消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(500);
                            finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    },
                    s -> System.out.println("消息接收失败"));
            System.out.println("work2开始接收消息");
            //加上该代码,方法回调才生效。
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //先关闭通道再关闭连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

先启动消费者,再启动生产者。

  • 消费者2处理消息量应该是消费者1的4倍左右
  • 一个2000毫秒,一个500毫秒。

4. RabbitMq使用场景

4.1 同步异步的问题(串行)

  • 串行方式:将订单信息写入数据库成功后,发送订单通知短信,再发送订单通知邮件。
  • 需要等以上三个任务全部完成后,才能返回给客户端。(同一个线程,由上向下依次执行。)
public void makeOrder(){
    // 1 :保存订单 
    orderService.saveOrder();
    // 2: 发送短信服务
    messageService.sendSMS("order");
    // 3: 发送email服务
    emailService.sendEmail("order");
    // 4: 发送APP服务
    appService.sendApp("order");    
}

解决方案:采用并行方式

  • 并行方式:将订单信息写入数据库成功后,发送邮件的同时,发送短信。且同时返回给客户端。
  • 与串行的差别是,并行的方式可以提高处理的时间。
  • 不用等待短信和邮件发送成功,即可通过开启一个新线程将订单发送给客户端。

public void makeOrder(){
    // 1 :保存订单 
    orderService.saveOrder();
   // 相关发送
   relationMessage();
}
public void relationMessage(){
    // 异步
     theadpool.submit(new Callable<Object>{
         public Object call(){
             // 2: 发送短信服务  
             messageService.sendSMS("order");
         }
     })
    // 异步
     theadpool.submit(new Callable<Object>{
         public Object call(){
              // 3: 发送email服务
            emailService.sendEmail("order");
         }
     }
    // 异步
     theadpool.submit(new Callable<Object>{
       public Object call(){
             // 4: 发送客户端服务
             appService.sendApp("order");
         }
     })
}

存在问题:

1:耦合度高
2:需要自己写线程池自己维护成本太高
3:出现了消息可能会丢失,需要你自己做消息补偿
4:如何保证消息的可靠性你自己写
5:如果服务器承载不了,你需要自己去写高可用

采用异步消息队列的方式

好处

1:完全解耦,用MQ建立桥接
2:有独立的线程池和运行模型
3:MQ有持久化功能
4:死信队列和消息转移的等
5:HA镜像模型高可用。

按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。

public void makeOrder(){
    // 1 :保存订单 
    orderService.saveOrder();   
    rabbitTemplate.convertSend("ex","2","消息内容");
}

4.2 解决高耦合问题

用户下单成功后,需要向其再添加推送微信消息。

  • 如果是之前异步处理,需要修改代码。
  • 如果采用异步消息队列的方式,只需要在微信服务中从队列获取数据即可。

4.3 流量削峰

举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

5. SpringBoot整合RabbitMQ

5.1 SpringBoot整合RabbitMQ-----fanout模式

  • 发布订阅模式
    • 对应交换机类型:fanout
    • 特点:Fanout—发布与订阅模式,是一种广播机制,需要绑定关系,它是没有路由key的模式。

5.1.1 生产者

  1. 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置文件
# 服务端口
server:
  port: 8083
# 配置rabbitmq服务
spring:
  rabbitmq:
    username: admin
    password: 123
    virtual-host: /
    host: 8.141.52.45
    port: 5672
  1. 创建service
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void makeOrderFanout(Long userId, Long productId, int num) {
        // 1: 模拟用户下单
        String orderNumer = UUID.randomUUID().toString();
        // 2: 根据商品id productId 去查询商品的库存
        // int numstore = productSerivce.getProductNum(productId);
        // 3:判断库存是否充足
        // if(num >  numstore ){ return  "商品库存不足..."; }
        // 4: 下单逻辑
        // orderService.saveOrder(order);
        // 5: 下单成功要扣减库存
        // 6: 下单完成以后
        System.out.println("fanout用户 " + userId + ",订单编号是:" + orderNumer);
        // 发送订单信息给RabbitMQ fanout_order_exchange交换机,交换机不存在的话就创建
        rabbitTemplate.convertAndSend("fanout_order_exchange", "", orderNumer);
    }

}
  1. 配置类
  • 要保证beanName唯一
import org.springframework.amqp.core.*;

@Configuration
public class FanoutRabbitConfig {

    //创建Fanout交换机
    @Bean
    public FanoutExchange fanoutOrderExchange() {
        return new FanoutExchange("fanout_order_exchange", true, false);
    }

    //创建三个队列
    @Bean
    public Queue emailFanoutQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        return new Queue("email.fanout.queue", true,false,false);
    }
    @Bean
    public Queue smsFanoutQueue() {
        return new Queue("sms.fanout.queue", true,false,false);
    }
    @Bean
    public Queue weixinFanoutQueue() {
        return new Queue("weixin.fanout.queue", true,false,false);
    }


    //建立绑定关系
    @Bean
    public Binding bindingDirect1() {
        return BindingBuilder.bind(weixinFanoutQueue()).to(fanoutOrderExchange());
    }
    @Bean
    public Binding bindingDirect2() {
        return BindingBuilder.bind(smsFanoutQueue()).to(fanoutOrderExchange());
    }
    @Bean
    public Binding bindingDirect3() {
        return BindingBuilder.bind(emailFanoutQueue()).to(fanoutOrderExchange());
    }

  1. controller
@RestController
public class MyController {
    @Autowired
    OrderService orderService;

    @RequestMapping("/order/fanout")
    public void makeOrder() throws Exception {
        for (int i = 1; i <= 10; i++) {
            Thread.sleep(1000);
            Long userId = 100L + i;
            Long productId = 10001L + i;
            int num = 10;
            orderService.makeOrderFanout(userId, productId, num);
        }
    }
}

此时访问/order请求,就会生成交换机,队列。及绑定关系以及在队列中添加10条数据。

5.1.2 消费者

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

service

@RabbitListener(queues = "email.fanout.queue")  //通过注解绑定队列
@Component
public class EmailService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("email-------------->" + message);
    }
}
-------------------------------------------------------------------
@RabbitListener(queues = "sms.fanout.queue")  //通过注解绑定队列
@Component
public class SmsService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("sms-------------->" + message);
    }
}
-------------------------------------------------------------------
@RabbitListener(queues = "weixin.fanout.queue")  //通过注解绑定队列
@Component
public class WeiXinService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("weixin-------------->" + message);
    }
}

由于service是一个组件,在springboot启动的时间就加载

  • 所以当消息队列中存在数据时,消费者项目一启动就进行消费。


5.2 SpringBoot整合RabbitMQ-----direct模式

  • 交换机类型:direct
  • 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。

5.2.1 生产者

配置类

  • bean名要唯一
  • 建立绑定关系时通过with()方法创建路由key。
@Configuration
public class DirectRabbitConfig {

    //创建direct交换机
    @Bean
    public DirectExchange directOrderExchange() {
        return new DirectExchange("direct_order_exchange", true, false);
    }

    //创建三个队列
    @Bean
    public Queue emailDirectQueue() {
        return new Queue("email.direct.queue", true,false,false);
    }
    @Bean
    public Queue smsDirectQueue() {
        return new Queue("sms.direct.queue", true,false,false);
    }
    @Bean
    public Queue weixinDirectQueue() {
        return new Queue("weixin.direct.queue", true,false,false);
    }

    //建立绑定关系
    @Bean
    public Binding bindingDirect4() {
        return BindingBuilder.bind(weixinDirectQueue()).to(directOrderExchange()).with("weixin");
    }
    @Bean
    public Binding bindingDirect5() {
        return BindingBuilder.bind(smsDirectQueue()).to(directOrderExchange()).with("sms");
    }
    @Bean
    public Binding bindingDirect6() {
        return BindingBuilder.bind(emailDirectQueue()).to(directOrderExchange()).with("email");
    }
}

Service

  • 在业务逻辑侧确定将消息发给哪些符合路由key的队列。
@Override
public void makeOrderDirect(Long userId, Long productId, int num) {
    String orderNumer = UUID.randomUUID().toString();
    System.out.println("direct用户 " + userId + ",订单编号是:" + orderNumer);
    //发微信和邮件给对应的队列
    rabbitTemplate.convertAndSend("direct_order_exchange", "email", orderNumer);
    rabbitTemplate.convertAndSend("direct_order_exchange", "weixin", orderNumer);
}

Controller

  • 发送请求,emailweixin两个队列会受到数据。
@RequestMapping("/order/direct")
public void makeOrderDirect() throws Exception {
    for (int i = 1; i <= 10; i++) {
        Thread.sleep(1000);
        Long userId = 100L + i;
        Long productId = 10001L + i;
        int num = 10;
        orderService.makeOrderDirect(userId, productId, num);
    }
}

5.2.2 消费者

  • 只需要修改上方的队列名即可。
@RabbitListener(queues = "email.direct.queue")  //通过注解绑定队列
@Component
public class EmailService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("email-------------->" + message);
    }
}
-------------------------------------------------------------------
@RabbitListener(queues = "sms.direct.queue")  //通过注解绑定队列
@Component
public class SmsService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("sms-------------->" + message);
    }
}
-------------------------------------------------------------------
@RabbitListener(queues = "weixin.direct.queue")  //通过注解绑定队列
@Component
public class WeiXinService {
    
    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("weixin-------------->" + message);
    }
}

只有email和微信能收到数据且消费数据。

5.3 SpringBoot整合RabbitMQ-----topic模式

  • 对应交换机类型:topic
  • 特点:模糊的routing-key的匹配模式

交换机、队列、路由key的定义及其绑定关系可以在消费者方通过注解的方式实现。

5.3.1 生产者

Service

@Override
public void makeOrderTopic(Long userId, Long productId, int num) {
    String orderNumer = UUID.randomUUID().toString();
    System.out.println("topic用户 " + userId + ",订单编号是:" + orderNumer);
    //发微信和邮件给对应的队列
    rabbitTemplate.convertAndSend("topic_order_exchange", "com.email.weixin.xxx", orderNumer);
}

Controller

@RequestMapping("/order/topic")
  public void makeOrderTopic() throws Exception {
      for (int i = 1; i <= 10; i++) {
          Thread.sleep(1000);
          Long userId = 100L + i;
          Long productId = 10001L + i;
          int num = 10;
          orderService.makeOrderTopic(userId, productId, num);
      }
  }

通过注解在消费者方定义交换机和队列,先不要启动生产者,因为此时交换机不存在。

5.3.2 消费者

Service

@RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(value = "email.topic.queue",durable = "true",autoDelete = "false"),
                exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
                key = "*.email.#"
        ))  //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class EmailTopicService {

    @RabbitHandler  //代表此方法是一个消息接收的方法。不要有返回值
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("emailTopic-------------->" + message);
    }
}
----------------------------------------------------------------------------------------------------
@RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(value = "sms.topic.queue",durable = "true",autoDelete = "false"),
                exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
                key = "#.sms.#"
        ))  //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class SmsTopicService {

    // @RabbitHandler 代表此方法是一个消息接收的方法。不要有返回值
    @RabbitHandler
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("sms-------------->" + message);
    }
}
----------------------------------------------------------------------------------------------------
@RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(value = "weixin.topic.queue",durable = "true",autoDelete = "false"),
                exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
                key = "#.weixin.*"
        ))  //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class WeiXinTopicService {

    // @RabbitHandler 代表此方法是一个消息接收的方法。不要有返回值
    @RabbitHandler
    public void messageReceive(String message){
        // 此处省略发邮件的逻辑
        System.out.println("weixin-------------->" + message);
    }
}

通过注解进行交换机和队列之间的绑定。

根据生产者的路由模糊匹配规则

  • email和weixin符合条件。这两个队列将收到数据。
//发微信和邮件给对应的队列
rabbitTemplate.convertAndSend("topic_order_exchange", "com.email.weixin.xxx", orderNumer);

生产订单数据。

消费订单数据。

建议

  • 建议使用配置类来管理交换机和队列之间的关系,方便管理和维护。
  • 之后的死信队列、延时队列需要。
  • 配置类在消费者方进行配置(创建交换机、队列及其绑定关系)。
  • 先启动消费者服务,再启动生产者服务。

6. RabbitMQ高级用法---过期时间TTL

过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。
RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置

  • 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
  • 第二种方法是对消息进行单独设置,每条消息TTL可以不同。(不建议使用)
  • 如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。

消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。

应用场景

  • 如果用户下单超过20分钟未付款,将该订单传入死信队列。(不会真正删除)。

6.1 设置队列过期时间

配置类

@Configuration
public class TTLRabbitConfig {
    //创建direct交换机
    @Bean
    public DirectExchange directTTLExchange() {
        return new DirectExchange("ttl_direct_exchange", true, false);
    }
    //创建一个队列
    @Bean
    public Queue ttlDirectQueue() {
        //添加过期时间5秒,将参数传入
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-message-ttl",5000);
        return new Queue("ttl.direct.queue", true,false,false,map);
    }
    //建立绑定关系
    @Bean
    public Binding bindingDirect7() {
        return BindingBuilder.bind(ttlDirectQueue()).to(directTTLExchange()).with("ttl");
    }
}

启动项目,访问请求,10条数据生产完毕。

5秒之后,未有消费者进行消费,数据自动删除。

6.2 设置消息过期时间

  • 新建队列时不用传入参数,发送数据时传入设置项。
@Override
public void makeTTL(Long userId, Long productId, int num) {
    String orderNumer = UUID.randomUUID().toString();
    System.out.println("ttl用户 " + userId + ",订单编号是:" + orderNumer);

    MessagePostProcessor processor = new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
           message.getMessageProperties().setExpiration("5000");
           message.getMessageProperties().setContentEncoding("UTF-8");
           return message;
        }
    };
    //发送数据时传入设置项。
    rabbitTemplate.convertAndSend("ttl_direct_exchange", "ttl", orderNumer,processor);
}

7. RabbitMQ高级用法---死信队列

DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。

消息变成死信,可能是由于以下的原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度

DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange 指定交换机即可。

7.1 配置死信交换机和死信队列

如果在线上碰到一个需要修改配置的交换机或队列,不要删除,新建一个新的交换机或队列和之前的产生关系。

@Configuration
public class TTLRabbitConfig {
    //创建direct交换机
    @Bean
    public DirectExchange directTTLExchange() {
        return new DirectExchange("ttl_direct_exchange", true, false);
    }
    //创建死信交换机
    @Bean
    public DirectExchange directDeadExchange() {
        return new DirectExchange("ttl_dead_exchange", true, false);
    }
    //创建死信队列,数据过期后会被放到死信队列中
    @Bean
    public Queue ttlDeadQueue(){
        return new Queue("ttl.dead.queue",true,false,false);
    }
    //死信交换机和死信队列进行绑定
    @Bean
    public Binding bindingDirect8() {
        return BindingBuilder.bind(ttlDeadQueue()).to(directDeadExchange()).with("dead");
    }

    //创建一个队列
    @Bean
    public Queue ttlDirectQueue() {
        //添加过期时间5秒
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-message-ttl",5000);
        map.put("x-dead-letter-exchange","ttl_dead_exchange");
        //direct模式需要设置key
        map.put("x-dead-letter-routing-key","dead");
        return new Queue("ttl.direct.queue", true,false,false,map);
    }
    //建立绑定关系
    @Bean
    public Binding bindingDirect7() {
        return BindingBuilder.bind(ttlDirectQueue()).to(directTTLExchange()).with("ttl");
    }

}

8. RabbitMQ高级用法---内存磁盘和监控

posted @ 2022-02-17 18:02  初夏那片海  阅读(42)  评论(0)    收藏  举报