Message Queue

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。它是应用程序和应用程序之间的通信方法。

为什么使用MQ?

如此图,mq总结为三个好处

应用解耦

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

异步提速

使用了mq,用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)。

否则就是920ms(300+300+300+20=920ms),mq显然提升了用户体验和系统吞吐量。

削峰填谷

举个例子,如果订单系统最多能处理一千次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两千次下单操作系统是处理不了的,只能限制订单超过一千后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。 简单来说: 就是在访问量剧增的情况下,但是应用仍然不能停,比如“双十一”下单的人多,但是淘宝这个应用仍然要运行,所以就可以使用消息中间件采用队列的形式减少突然访问的压力,使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。提高系统的稳定性。

使用MQ的劣势

系统可用性降低

系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。

系统复杂度提高

MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。

一致性问题

A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。使用mq后要保证数据处理的一致性。

常见的MQ组件

目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征

RabbitMQ的概述

RabbitMQ的概念

RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message,可复用的企业消息系统,是当前最主流的消息中间件之一。

RabbitMQ的原理

名词解释:

  1. Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

  2. Connection:publisher/consumer 和 broker 之间的 TCP 连接

  3. Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

  4. Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

  5. Queue:消息最终被送到这里等待 consumer 取走

  6. Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

  7. Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Java使用RabbitMQ 

RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。 官网对应模式介绍:RabbitMQ Tutorials — RabbitMQ

安装完RabbitMQ后,启动服务,访问地址ip:15672即可进入登录页面

默认账号密码为guest

提供了一个图形化管理页面。

这里我们使用Java来操作RabbitMQ

首先创建一个maven工程,然后在此工程下再创建两个maven工程,一个名为product,一个名为consumer,整体结构如下,这里介绍五种模式。product生产消息,consumer接受消息。

在父工程导入依赖,子工程共享依赖。

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

    <groupId>com.aaa</groupId>
    <artifactId>qy168-rabbitmq</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>qy168-product</module>
        <module>qy168-consumer</module>
    </modules>

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


</project>

simple (简单模式)

消息生产者


public class HelloProduct {
    public static void main(String[] args) throws Exception {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1 这里为安装rabbitMq服务的ip
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest 如果使用默认账号 不用写此行代码
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest 如果使用默认账号 不用写此行代码
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 如果使用默认主机账号 不用写此行代码

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();

        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        /*
        String queue,队列的名称--命名规则就可以
        boolean durable, 是否持久化。--
        boolean exclusive, 是否独占--当前channel是否独占该队列
        boolean autoDelete, 是否自动删除该队列。
          Map<String, Object> arguments:该队列的属性参数--null
         */
        channel.queueDeclare("hello_queue",true,false,false,null);

        //5. 发送消息
        /*
        String exchange, 交换机的名称--像简单模式没有交换机 ""
        String routingKey, 路由key. 像简单模式 默认给定为队列的名称
         BasicProperties props,消息的属性--现在给定null.
         byte[] body: 消息的内容
         */
        String msg="hello rabbitmq  lam qy168~~~~~~~~~~~~";
        channel.basicPublish("","hello_queue",null,msg.getBytes());

        //6.关闭资源。
        channel.close();
        connection.close();


    }
}

消费者监听方

public class HelloConsumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1 这里为安装rabbitMq服务的ip
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest 如果使用默认账号 不用写此行代码
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest 如果使用默认账号 不用写此行代码
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 如果使用默认主机账号 不用写此行代码

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        channel.queueDeclare("hello_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("hello_queue", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

先运行生产者方代码,可以在图形化界面查看到代码生成的交换机exchange和队列queue还有队列所带的message,再运行消费者,就能够收到生产者的消息了。以下几种模式不再赘述。

Work queues(工作模式)


public class WorkProduct {
    public static void main(String[] args) throws Exception {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();

        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        /*
        String queue,队列的名称--命名规则就可以
        boolean durable, 是否持久化。--
        boolean exclusive, 是否独占--当前channel是否独占该队列
        boolean autoDelete, 是否自动删除该队列。
          Map<String, Object> arguments:该队列的属性参数--null
         */
        channel.queueDeclare("work_queue",true,false,false,null);

        //5. 发送消息
        /*
        String exchange, 交换机的名称--像简单模式没有交换机 ""
        String routingKey, 路由key. 像简单模式 默认给定为队列的名称
         BasicProperties props,消息的属性--现在给定null.
         byte[] body: 消息的内容
         */
        for (int i = 0; i < 10; i++) {
            String msg="work rabbitmq  lam qy168~~~~~~~~~~~~"+i;
            channel.basicPublish("","work_queue",null,msg.getBytes());
        }


        //6.关闭资源。
        channel.close();
        connection.close();


    }
}
public class WorkConsumer01 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("work_queue", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}
public class WorkConsumer02 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("work_queue", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

 执行代码后会发现,两个消费者轮询接受生产者的消息。

Publish/Subscribe(发布与订阅模式)

在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:

  • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

  • C:消费者,消息的接收者,会一直等待消息到来

  • Queue:消息队列,接收消息、缓存消息

  • Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:

    1. Fanout:广播,将消息交给所有绑定到交换机的队列

    2. Direct:定向,把消息交给符合指定routing key 的队列

    3. Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Exchange(交换机):只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!


public class PublishSubscribeProduct {
    public static void main(String[] args) throws IOException, TimeoutException {

            //1.设置连接对象的信息
            ConnectionFactory factory=new ConnectionFactory();
            factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
            factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
            factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
            factory.setPassword("123456");//设置rabbitmq的密码 默认guest
            factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

            //2. 获取连接对象
            Connection connection=factory.newConnection();
            //3. 获取Channel信道
            Channel channel = connection.createChannel();

            //4. 创建交换机
        /*
        String exchange,交换机的名称
         BuiltinExchangeType type, 交换机的类型
         boolean durable: 是否持久化
         */
            channel.exchangeDeclare("fanout_exchange", BuiltinExchangeType.FANOUT,true);

            //5. 创建队列
            channel.queueDeclare("fanout_queue01",true,false,false,null);
            channel.queueDeclare("fanout_queue02",true,false,false,null);

            //6.交换机和队列绑定
        /*
        String queue,队列名
         String exchange,交换机名
         String routingKey: 路由key 因为广播模式没有路由key  ”“
         */
            channel.queueBind("fanout_queue01","fanout_exchange","");
            channel.queueBind("fanout_queue02","fanout_exchange","");

            //7. 发送消息
            String msg="这时一个发布订阅工作模式";
            channel.basicPublish("fanout_exchange", "", null, msg.getBytes());


            channel.close();
            connection.close();

        }
    }

public class PublishSubscribeConsumer01 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("fanout_queue01", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

 


public class PublishSubscribeConsumer02 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
        factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
        factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
        factory.setPassword("123456");//设置rabbitmq的密码 默认guest
        factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("fanout_queue02", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

 Routing(路由模式)

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)

  • 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey

  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息

  • P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key

  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列

  • C1:消费者,其所在队列指定了需要 routing key 为 error 的消息

  • C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息

 


public class DirectProduct {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT,true);

        channel.queueDeclare("direct_queue01",true,false,false,null);
        channel.queueDeclare("direct_queue02",true,false,false,null);

        channel.queueBind("direct_queue01","direct_exchange","error");

        channel.queueBind("direct_queue02","direct_exchange","error");
        channel.queueBind("direct_queue02","direct_exchange","info");
        channel.queueBind("direct_queue02","direct_exchange","warning");


        String msg="路由模式发送消息";
        channel.basicPublish("direct_exchange","error",null,msg.getBytes());


    }
}

public class DirectConsumer01 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("direct_queue01", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

public class DirectConsumer02 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("direct_queue02", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

 Topics(主题模式)

Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符。
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert 
通配符规则:# 匹配一个或多个词,* 只匹配一个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert


public class TopicProduct {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("topic_exchange", BuiltinExchangeType.TOPIC,true);

        channel.queueDeclare("topic_queue01",true,false,false,null);
        channel.queueDeclare("topic_queue02",true,false,false,null);

        channel.queueBind("topic_queue01","topic_exchange","*.orange.*");

        channel.queueBind("topic_queue02","topic_exchange","*.*.rabbit");
        channel.queueBind("topic_queue02","topic_exchange","lazy.#");


        String msg="主题模式发送消息";
        channel.basicPublish("topic_exchange","lazy",null,msg.getBytes());

    }
}

public class TopicConsumer01 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("topic_queue01", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}
public class TopicConsumer02 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.设置连接对象的信息
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1

        //2. 获取连接对象
        Connection connection=factory.newConnection();
        //3. 获取Channel信道
        Channel channel = connection.createChannel();
        //4. 创建队列--如果队列不存在则创建---如果存在则不创建.
        //channel.queueDeclare("work_queue",true,false,false,null);
        //5. 监听消息
        /*
        String queue, 监听的队列名
        boolean autoAck, 是否自动确认。
        Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
         */
        DefaultConsumer callback = new DefaultConsumer(channel) {
            //一定重写该方法。
            /*
                String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                byte[] body: 接受的消息---byte数组
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body);
                System.out.println("消息的内容:" + msg);
            }
        };
        channel.basicConsume("topic_queue02", true, callback);

        //不能关闭connection和channel 关了就监听不了了

    }
}

 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。

springboot整合rabbitmq

1、先创建好Springboot项目

这里父模块和子模块都为sprngboot项目,子模块共享父模块的依赖

2、在父模块导入依赖

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

    <groupId>com.aaa</groupId>
    <artifactId>qt168-springboot-rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>
</project>

3、生产者端配置yml文件

server.port=8088

spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=192.168.184.130

spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true

confirm returns 先写上,后面会讲

4、定义交换机,队列以及绑定关系的配置类,因为这里有现成的交换机和队列(刚刚在java工程创建的交换机和队列可以拿来用)

5、注入RabbitTemplate,调用方法,完成消息发送   


@SpringBootTest
class SpringbootRabbitProductApplicationTests {

    //springboot封装了一个工具类。该类提供了相应的方法。
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoadsPt() {
        //String exchange, String routingKey, Object message
        rabbitTemplate.convertAndSend("topic_exchange", "lazy.orange.aaa", "哈哈哈");

    }

6、消费者端编写yml配置,基本信息配置 定义监听类,使用@RabbitListener注解完成队列监听。

server.port=7777

spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=192.168.184.130

spring.rabbitmq.listener.simple.acknowledge-mode=manual

acknowledge 先写上 后面会讲 


@Component
public class MyRabbitListener {
    //把收到的消息封装到一个Message对象中
    @RabbitListener(queues = {"topic_queue01"})
    public void myListener01(Message message){
        byte[] body = message.getBody();
        String msg=new String(body);
        System.out.println("收到的消息内容:"+msg);
        System.out.println("根据消息写自己的业务代码");
    }
}

- SpringBoot提供了快速整合RabbitMQ的方式
- 基本信息再yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置,这里使用了现有的交换机和队列(java使用RabbitMQ所创建的),之后会再演示
- 生产端直接注入RabbitTemplate完成消息发送
- 消费端直接使用@RabbitListener完成消息接收

保证消息的稳定性

消息的可靠投递

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

- confirm 确认模式
- return  退回模式
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange 到 queue 投递失败则会返回一个 returnCallback 。

我们将利用这两个 callback 控制消息的可靠性投递

confirm和return的实现

1. 设置ConnectionFactory的publisher-confirm-type: correlated开启 确认模式。(上面已配置)

spring.rabbitmq.publisher-confirm-type=correlated (生产者yml文件)

2. 使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

spring.rabbitmq.listener.simple.acknowledge-mode=manual (消费者yml文件 先加上后面讲)

3. 设置ConnectionFactory的publisher-returns="true" 开启 退回模式。

spring.rabbitmq.publisher-returns=true (生产者yml文件)

4. 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后执行回调函数returnedMessage。

上代码:

生产者创建交换机和队列和绑定关系


@Configuration
public class RabbitMqConfig {
    public static String exchange_name01 = "aaa_exchange01";
    public static String queue_name01 = "aaa_queue01";
    public static String queue_name02 = "aaa_queue02";

    //创建交换机
    @Bean
    public Exchange exchange01() {
        Exchange exchang = ExchangeBuilder.topicExchange(exchange_name01).durable(true).build();
        return exchang;
    }

    //创建队列
    @Bean
    public Queue queue01() {
        Queue queue = QueueBuilder.durable(queue_name01).build();
        return queue;
    }

    @Bean
    public Queue queue02() {
        //为队列设置过期时间
       // Queue queue = QueueBuilder.durable(queue_name02).withArgument("x-message-ttl",20000).build();
        Queue queue = QueueBuilder.durable(queue_name02).build();
        return queue;
    }

    //创建队列和交换机的绑定关系
    @Bean
    public Binding binding01() {
        Binding binding = BindingBuilder.bind(queue01()).to(exchange01()).with("*.orange.*").noargs();
        return binding;
    }

    @Bean
    public Binding binding02() {
        Binding binding = BindingBuilder.bind(queue02()).to(exchange01()).with("*.*.rabbit").noargs();
        return binding;
    }

    @Bean
    public Binding binding03() {
        Binding binding = BindingBuilder.bind(queue02()).to(exchange01()).with("lazy.#").noargs();
        return binding;
    }

}

测试类中使用机制发送消息

@SpringBootTest
class SpringbootRabbitProductApplicationTests {

    //springboot封装了一个工具类。该类提供了相应的方法。
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoadsPt() {
        //String exchange, String routingKey, Object message
        rabbitTemplate.convertAndSend("topic_exchange", "lazy.orange.aaa", "哈哈哈");

    }
    @Test
    void contextLoads() {
        Map<String, Object> map = new HashMap<>();
        map.put("orderId", UUID.randomUUID());
        map.put("productId", 1);
        map.put("num", 15);
        rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"lazy.orange.rabbit", JSON.toJSONString(map));

    }


    //生产者到交换机 confirm机制
    @Test
    public void testConfirm(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    System.out.println("发送消息成功");
                }else{
                    System.out.println("发送消息到交换机失败");
                }
            }
        });
        Map<String, Object> map = new HashMap<>();
        map.put("orderId", UUID.randomUUID());
        map.put("productId", 2);
        map.put("num", 15);
        rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"aaa.bbb.rabbit", JSON.toJSONString(map));
    }



    //交换机到队列 return 机制
    @Test
    void returenTest() {
      rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
          @Override
          public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
              System.out.println("返还消息:"+message);
              System.out.println("失败原因"+replyText);
          }
      });
        Map<String, Object> map = new HashMap<>();
        map.put("orderId", UUID.randomUUID());
        map.put("productId", 3);
        map.put("num", 15);

        //给消息设置过期时间 如果队列也设置了过期时间 那么谁的时间短根据谁过期
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){

            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("30000");
                //给消息设置Id
                //message.getMessageProperties().setMessageId();
                return message;
            }
        };

        rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"aaa.bbb.rabbit", JSON.toJSONString(map),messagePostProcessor);
    }

    //队列到消费者的消息可靠性机制要在 消费者端进行


    @Test
    public void testDead(){
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("pt_exchange","","消息内容"+i);
        }

    }
}

消费者监听消息


@Component
public class MyRabbitListener {
    //把收到的消息封装到一个Message对象中
    @RabbitListener(queues = {"topic_queue01"})
    public void myListener01(Message message){
        byte[] body = message.getBody();
        String msg=new String(body);
        System.out.println("收到的消息内容:"+msg);
        System.out.println("根据消息写自己的业务代码");
    }

    @RabbitListener(queues = {"aaa_queue02"})
    public void myListener02(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try{
         byte[] body = message.getBody();
         String msg=new String(body);
         Map map = JSON.parseObject(msg, Map.class);
         System.out.println("收到的消息内容:"+map);
         // int c = 1/0;
         System.out.println("根据消息写自己的业务代码");
         //deliveryTag 消息标识  boolean multiple 是否把该消息之前的消息也确认掉
            channel.basicAck(deliveryTag,true);
     }catch (Exception e){
            System.out.println("程序出现意外情况");
            //requeue:true 要求继续发送消息
            channel.basicNack(deliveryTag,true,true);
     }
    }

}

ACK确认机制

此机制在上面的生产者测试类代码中顺便加入了,配置文件内容也有添加,在此解释:

多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除,这样就解决了,即使一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。

ack指Acknowledge,确认,表示消费端收到消息后的确认方式。
有三种确认方式:
自动确认:acknowledge=none
手动确认:acknowledge=manual
根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,并且不常用,不作讲解)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息队列中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值