RabbitMQ 消息队列

RabbitMQ 消息队列

一、基础

1.MQ的概述

1.1MQ概述

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。

小结:
  • MQ,消息队列,存储消息的中间件

  • 分布式系统通信两种方式:直接远程调用 和 借助第三方 完成间接通信

  • 发送方称为生产者,接收方称为消费者

1.2MQ的优势和劣势

优势:
  • 应用解耦

  • 异步提述

  • 削峰填谷

劣势:
  • 系统可用性降低

  • 系统复杂度提高

  • 一致性问题

1.3MQ的优势

1.应用解耦

如果使用直接调用,很有可能导致耦合性高,容错性低,可维护性低

比如:

  1. 当订单系统调用库存系统时,报错,则整个系统都使用不了

  2. 当系统需要频繁地添加减少系统时,耦合性高则需要改动的代码就会很多

而MQ作为消息传输的中间件,可以提升容错性和可维护性

  1. 三个系统可以从MQ获取数据,当库存发生错误时,不会影响到其它的系统

  2. 可以在MQ那添加减少系统的链接

2.异步提速

一个下单操作耗时:20+300+300+300=920ms

用户点击完下单按钮后,需要等待920ms才能得到下单响应,太慢!

3.削峰填谷
小结:
  • 应用解耦:提高系统容错性和可维护性

  • 异步提速:提升用户体验和系统吞吐量

  • 削峰填谷:提高系统稳定性

1.4MQ的劣势

  • 系统可用性降低

    系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。如何保证MQ的高可用?

  • 系统复杂度提高

    MQ的加入增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

  • 一致性问题

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

小结

既然MQ有优势也有劣势,那么使用MQ需要满足什么条件呢?

  1. 生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。

  2. 容许短暂的不一致性

  3. 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。

1.5常见的MQ产品

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

1.6RabbitMQ 简介

AMOP,即Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP规范发布。类比HTTP

2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ1.0发布。RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。

RabbitMQ中的相关概念:

  • Broker:接收和分发消息的应用,RabbitMQ Server就是Messgae Broker

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

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

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

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

  • Queue:消息最终被送到这里等待consummer取走

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

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

1.7 JMS

  • JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件的API

  • JMS是javaEE规范中的一种,类比JDBC

  • 很多消息中间件都实现了JMS规范,例如:ActiveMQ,RabbitMQ官方没有提供JMS的实现包,但是开源社区有

小结
  1. RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品

  2. RabbitMQ提供了6种工作模式,我们学习5种。这是今天的重点。

  3. AMQP是协议,类比HTTP

  4. JMS是API规范接口,类比JDBC

2.RabbitMQ的安装和配置

由于RabbitMQ的编程语言是Elang,所以在下载RabbitMQ的时候我们要先下载Elang包,并且根据官方查看是否匹配!!(官方有教程可以再深入学习RabbitMQ)

2.1 安装说明

详细查看 安装Windows RabbitMQ.pdf 文档

2.2 用户以及Virual Hosts配置

2.2.1 用户角色

RabbitMQ在安装好后,可以访问http://localhost:15672 ;其自带了guest/guest的用户名和密码;如果需要创建自 定义用户;那么也可以登录管理界面后,如下操作:

角色说明:

  1. 超级管理员(administrator)

    可登陆管理控制台,可查看所有的信息 ,并且可以对用户,策略(policy)进行操作

  2. 监控者(monitoring)

    可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

  3. 策略制定者(policymaker)

    可登陆管理控制台,同时可以对policy进行管理 。但无法查看节点的相关信息(上图红框标识的部分)

  4. 普通管理者(management)

    仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理

  5. 其他

    无法登陆管理控制台,通常就是普通的生产者和消费者

2.2.2 Virtal Hosts配置

像mysql拥有数据库的概念并且可以指定用户对库和表等操作的权限 。RabbitMQ也有类似的权限管理 ;在RabbitMQ中可以虚拟消息服务器Virtal Host ,每个Virtal Hosts相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通 。相当于mysql的db。Virtual Name一般以/开头。

  1. 创建Virtual Hosts

  2. 设置Virtual Hosts权限

3.RabbitMQ入门

3.1入门程序

需求:使用简单模式完成消息传递

步骤:

  1. 创建工程(生成者、消费者)

  2. 分别添加依赖

  3. 编写生产者发送消息

  4. 消费者接受消息

3.2 搭建示例工程

3.1.1创建工程

3.3 编写生产者

编写消息生产者com.chen.rabbitmq.simple.Producer

复制代码
package com.chen.producer;
​
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
/*
* 发送消息
* */
public class Producer_HelloWorld {
    //static final String QUEUE_NAME="simple_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
          //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
          //默认为localhost
        connectionFactory.setHost("localhost");
          //连接端口,默认为 5672
        connectionFactory.setPort(5672);
          //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
          //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
          //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");
​
        //3.创建连接
        Connection connection=connectionFactory.newConnection();
​
        //4.创建频道
        Channel channel = connection.createChannel();
​
        //5.声明(创建)队列
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
        validateQueueNameLength(queue);
        return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
    }
​
        * 参数一queue:队列名称
        * 参数二durable:是否定义持久化队列(当mq重启之后,还在)
        * 参数三exclusive:
                 是否独占本次连接,只能有一个消费者监听这队列
                 当Connection关闭时,是否删除队列
        * 参数四autoDelete:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
        * 参数五arguments:队列其它参数
        * */
        //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("hello_world",true,false,false,null);
​
        //6.发送消息
        /*
         *  public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
            this.basicPublish(exchange, routingKey, false, props, body);
            }*/
        /*
        * 1.exchange:交换机名称,如果没有指定则使用默认Defalut Exchange
        * 2.routingKey:路由key,简单模式可以传递队列名称
        * 3.props:配置消息
        * 4.body:发送消息内容*/
        String body="hello,rabbitmq";
        channel.basicPublish("","hello_world",null,body.getBytes());
​
        //7.释放资源
        channel.close();
        connection.close();
​
    }
}

点击队列名查看消息

3.3编写消费者

抽取创建connection的工具类com.chen.rabbitmq.util.ConnectionUtil;

复制代码
package com.itheima.rabbitmq.util;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("localhost");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/itcast");
//连接用户名;默认为guest
connectionFactory.setUsername("heima");
//连接密码;默认为guest
connectionFactory.setPassword("heima");
//创建连接
return connectionFactory.newConnection();
}
}

编写消息消费者com.itheima.rabbitmq.simple.Consumer

复制代码
package com.itheima.rabbitmq.simple;
import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消
息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.QUEUE_NAME, true, consumer);
//不关闭资源,应该一直监听消息
//channel.close();
//connection.close();
}
}

3.2 小结

上述的入门案例中其实使用的是如下的简单模式:

在上图的模型中,有以下概念:

  • P:生产者,也就是要发送消息的程序

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

  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息

在rabbitMQ中消息者是一定要到某个消息队列中去获取消息的

4.RabbitMQ工作模式

4.1Work queues工作队列模式

4.1.1 模式说明

Work Queue与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。

**应用场景:**对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度

4.1.2 代码

Work Queues与入门程序的简单模式的代码是几乎一样的;可以完全复制,并复制多一个消费者进行多个消费者同时消费消息的测试

1)生产者
复制代码
package com.chen.producer;
​
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
/*
* 发送消息
* */
public class Producer_WorkQueue {
    //static final String QUEUE_NAME="simple_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
          //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
          //默认为localhost
        connectionFactory.setHost("localhost");
          //连接端口,默认为 5672
        connectionFactory.setPort(5672);
          //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
          //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
          //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");
​
        //3.创建连接
        Connection connection=connectionFactory.newConnection();
​
        //4.创建频道
        Channel channel = connection.createChannel();
​
        //5.声明(创建)队列
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
        validateQueueNameLength(queue);
        return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
    }
​
        * 参数一queue:队列名称
        * 参数二durable:是否定义持久化队列(当mq重启之后,还在)
        * 参数三exclusive:
                 是否独占本次连接,只能有一个消费者监听这队列
                 当Connection关闭时,是否删除队列
        * 参数四autoDelete:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
        * 参数五arguments:队列其它参数
        * */
        //如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queue",true,false,false,null);
​
        //6.发送消息
        /*
         *  public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
            this.basicPublish(exchange, routingKey, false, props, body);
            }*/
        /*
        * 1.exchange:交换机名称,如果没有指定则使用默认Defalut Exchange
        * 2.routingKey:路由key,简单模式可以传递队列名称
        * 3.props:配置消息
        * 4.body:发送消息内容*/
        for (int i = 0; i < 10; i++) {
            String body=i+" hello,rabbitmq";
            channel.basicPublish("","work_queue",null,body.getBytes());
        }
​
        //7.释放资源
        channel.close();
        connection.close();
​
    }
}
​

修改原生简单模式的队列名

2)消费者1
复制代码
package com.chen.consumer;
​
import com.rabbitmq.client.*;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class Consumer_WorkQueue1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");
​
        //3.创建连接
        Connection connection=connectionFactory.newConnection();
​
        //4.创建频道
        Channel channel = connection.createChannel();
​
        //5.声明(创建)队列
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
        validateQueueNameLength(queue);
        return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
    }
​
        * 参数一queue:队列名称
        * 参数二durable:是否定义持久化队列(当mq重启之后,还在)
        * 参数三exclusive:
                 是否独占本次连接,只能有一个消费者监听这队列
                 当Connection关闭时,是否删除队列
        * 参数四autoDelete:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
        * 参数五arguments:队列其它参数
        * */
        //如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queue",true,false,false,null);
​
        /*
        * public String basicConsume(String queue, boolean autoAck, Consumer callback)
        * 参数:
        * 1.queue:队列名称
        * 2.outAck:是否自动确认
        * 3.callback:回调对象
        * */
​
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
            }
        };
        channel.basicConsume("work_queue",true,consumer);
​
        //不关闭资源,应该一直监听消息
        // channel.close();
        // connection.close();
    }
}
3)消费者2
复制代码
package com.chen.consumer;
​
import com.rabbitmq.client.*;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class Consumer_WorkQueue2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");
​
        //3.创建连接
        Connection connection=connectionFactory.newConnection();
​
        //4.创建频道
        Channel channel = connection.createChannel();
​
        //5.声明(创建)队列
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
        validateQueueNameLength(queue);
        return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
    }
​
        * 参数一queue:队列名称
        * 参数二durable:是否定义持久化队列(当mq重启之后,还在)
        * 参数三exclusive:
                 是否独占本次连接,只能有一个消费者监听这队列
                 当Connection关闭时,是否删除队列
        * 参数四autoDelete:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
        * 参数五arguments:队列其它参数
        * */
        //如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queue",true,false,false,null);
​
        /*
        * public String basicConsume(String queue, boolean autoAck, Consumer callback)
        * 参数:
        * 1.queue:队列名称
        * 2.outAck:是否自动确认
        * 3.callback:回调对象
        * */
​
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
            }
        };
        channel.basicConsume("work_queue",true,consumer);
​
        //不关闭资源,应该一直监听消息
        // channel.close();
        // connection.close();
    }
}
4.1.3 测试

启动两个消费者,然后再启动生产者发送消息;到idea的两个消费者对应的控制台查看是否竞争者接收到消息。

4.1.4 小结

在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争关系

4.2 订阅模式类型

前面2个案例中,只有3个角色:

  • P:生产者,也就是要发送消息的程序

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

  • queue:消息队列,图中红色部分

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

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

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

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

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

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

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

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

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

4.3 Publish/Subscribe发布与订阅模式

4.3.1模式说明

发布订阅模式:

  1. 每个消费者监听自己的队列

  2. 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息

4.3.2 代码
1)生产者
复制代码
package com.chen.producer;
​
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class Producer_PubSub {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2.设置参数
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/itcast");
        connectionFactory.setUsername("chen");
        connectionFactory.setPassword("a3344048");
        //3.创建连接
        Connection connection = connectionFactory.newConnection();
        //4.创建channel
        Channel channel = connection.createChannel();
​
        /*
        * public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        * 参数:
        * 1.exchange:交换机名称
        * 2.type:交换机类型
             DIRECT("direct"),:定向
             FANOUT("fanout"),:扇形(广播),发送消息到每个与之绑定的队列
             TOPIC("topic"),:通配符的方式
             HEADERS("headers");参数匹配
​
        * 3.durable:是否持久化
        * 4.autoDelete:是否自动删除
        * 5.internal:内部使用 一般是false
        * 6.arguments:参数
        */
        String exchangeName="test_fanout";
        //5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
​
        //6.创建队列
        String queue1Name="test_fanout_queue1";
        String queue2Name="test_fanout_queue2";
        channel.queueDeclare(queue1Name,true,false,false,null);
        channel.queueDeclare(queue2Name,true,false,false,null);
        //7.绑定队列和交换机
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.BindOk queueBind(String queue, String exchange, String routingKey)
        参数:
        1.queue:队列名称
        2.exchange:交换机名称
        3.routingKey:路由键,绑定规则
        * */
        channel.queueBind(queue1Name,exchangeName,"");
        channel.queueBind(queue2Name,exchangeName,"");
        //8.发送消息
        String body="日志信息:张三调用了findAll方法...日志级别:info...";
        channel.basicPublish(exchangeName,"",null,body.getBytes());
​
        //9.关闭资源
        channel.close();
        connection.close();
    }
}
2)消费者1
复制代码
package com.chen.consumer;
​
import com.rabbitmq.client.*;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class Consumer_PubSub1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");
​
        //3.创建连接
        Connection connection=connectionFactory.newConnection();
​
        //4.创建频道
        Channel channel = connection.createChannel();
​
        //5.创建队列
        String queue1Name="test_fanout_queue1";
        String queue2Name="test_fanout_queue2";
​
        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
            }
        };
        channel.basicConsume(queue1Name,true,consumer);
    }
}

3)消费者2

复制代码
package com.chen.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer_PubSub2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");

        //3.创建连接
        Connection connection=connectionFactory.newConnection();

        //4.创建频道
        Channel channel = connection.createChannel();

        //5.创建队列
        String queue1Name="test_fanout_queue1";
        String queue2Name="test_fanout_queue2";

        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
            }
        };
        channel.basicConsume(queue2Name,true,consumer);
    }
}
4.3.3 测试

启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达广播的效果

在执行完测试代码后,其实RabbitMQ的管理后台找到Exchanges选项卡,点击fanout_exchange的交换机,可以查看如下的绑定:

4.3.4 小结

交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到

发布订阅模式与工作队列模式的区别

  1. 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机

  2. 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)

  3. 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机。

4.4 Routing 路由模式

4.4.1 模式说明

路由模式特点:

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

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

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

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

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

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

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

4.4.2 代码

在编码上与Publish/Subscribe发布与订阅模式的区别是交换机的类型为:Direct,还有队列绑定交换机的时候需要指定routing key。

1)生产者
复制代码
package com.chen.producer;

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer_routing {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2.设置参数
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/itcast");
        connectionFactory.setUsername("chen");
        connectionFactory.setPassword("a3344048");
        //3.创建连接
        Connection connection = connectionFactory.newConnection();
        //4.创建channel
        Channel channel = connection.createChannel();

        /*
        * public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        * 参数:
        * 1.exchange:交换机名称
        * 2.type:交换机类型
             DIRECT("direct"),:定向
             FANOUT("fanout"),:扇形(广播),发送消息到每个与之绑定的队列
             TOPIC("topic"),:通配符的方式
             HEADERS("headers");参数匹配

        * 3.durable:是否持久化
        * 4.autoDelete:是否自动删除
        * 5.internal:内部使用 一般是false
        * 6.arguments:参数
        */
        String exchangeName="test_direct";
        //5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,false,null);

        //6.创建队列
        String queue1Name="test_direct_queue1";
        String queue2Name="test_direct_queue2";
        channel.queueDeclare(queue1Name,true,false,false,null);
        channel.queueDeclare(queue2Name,true,false,false,null);
        //7.绑定队列和交换机
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.BindOk queueBind(String queue, String exchange, String routingKey)
        参数:
        1.queue:队列名称
        2.exchange:交换机名称
        3.routingKey:路由键,绑定规则
        * */
        channel.queueBind(queue1Name,exchangeName,"error");
        channel.queueBind(queue2Name,exchangeName,"info");
        channel.queueBind(queue2Name,exchangeName,"error");
        channel.queueBind(queue2Name,exchangeName,"warning");
        //8.发送消息
        String body="日志信息:张三调用了findAll方法...日志级别:info...";
        channel.basicPublish(exchangeName,"info",null,body.getBytes());

        //9.关闭资源
        channel.close();
        connection.close();
    }
}

输入队列的routingkey和发送消息规定key

2)消费者1
复制代码
package com.chen.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer_Routing1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");

        //3.创建连接
        Connection connection=connectionFactory.newConnection();

        //4.创建频道
        Channel channel = connection.createChannel();

        //5.创建队列
        String queue1Name="test_direct_queue1";
        String queue2Name="test_direct_queue2";

        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
                System.out.println("将日志打印到控制台");
            }
        };
        channel.basicConsume(queue1Name,true,consumer);
    }
}

3)消费者3

复制代码
package com.chen.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer_Routing2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");

        //3.创建连接
        Connection connection=connectionFactory.newConnection();

        //4.创建频道
        Channel channel = connection.createChannel();

        //5.创建队列
        String queue1Name="test_direct_queue1";
        String queue2Name="test_direct_queue2";

        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
                System.out.println("将日志存储到数据库");
            }
        };
        channel.basicConsume(queue2Name,true,consumer);
    }
}
4.4.3测试

启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routingkey对应队列的消息;到达按照需要接收的效果

在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击direct_exchange的交换机,可以查看到如下的绑定:

4.4.4 小结

注意:交换机的类型是direct

Routing模式要求队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列。

4.5 Topics通配符模式

Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以"."分割,例如:item.insert

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好一个词

举例:

item.#:能够匹配item.insert.abc或者item.insert

item.*:只能匹配item.insert

  • 红色Queue:绑定的是usa.#,因此凡是以usa.开头的routing key都会被匹配到

  • 黄色Queue:绑定的是#.new,因此凡是以.news结尾的routing key都会被匹配

4.5.2 代码
1)生产者

使用topic类型的Exchange,发送消息的routing key有3种:#.errororder.**.*

复制代码
package com.chen.producer;

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer_topics {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2.设置参数
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/itcast");
        connectionFactory.setUsername("chen");
        connectionFactory.setPassword("a3344048");
        //3.创建连接
        Connection connection = connectionFactory.newConnection();
        //4.创建channel
        Channel channel = connection.createChannel();

        /*
        * public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        * 参数:
        * 1.exchange:交换机名称
        * 2.type:交换机类型
             DIRECT("direct"),:定向
             FANOUT("fanout"),:扇形(广播),发送消息到每个与之绑定的队列
             TOPIC("topic"),:通配符的方式
             HEADERS("headers");参数匹配

        * 3.durable:是否持久化
        * 4.autoDelete:是否自动删除
        * 5.internal:内部使用 一般是false
        * 6.arguments:参数
        */
        String exchangeName="test_topics";
        //5.创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,false,null);

        //6.创建队列
        String queue1Name="test_topics_queue1";
        String queue2Name="test_topics_queue2";
        channel.queueDeclare(queue1Name,true,false,false,null);
        channel.queueDeclare(queue2Name,true,false,false,null);
        //7.绑定队列和交换机
        /*
        public com.rabbitmq.client.impl.AMQImpl.Queue.BindOk queueBind(String queue, String exchange, String routingKey)
        参数:
        1.queue:队列名称
        2.exchange:交换机名称
        3.routingKey:路由键,绑定规则
        * */
        //routing key 系统的名称.日志的级别
        //需求:所有error级别的日志存入数据库,所有order系统的日志存入数据库
        channel.queueBind(queue1Name,exchangeName,"#.error");
        channel.queueBind(queue1Name,exchangeName,"order.*");
        channel.queueBind(queue2Name,exchangeName,"*.*");
        //8.发送消息
        String body="日志信息:张三调用了findAll方法...日志级别:info...";
        channel.basicPublish(exchangeName,"goods.info",null,body.getBytes());

        //9.关闭资源
        channel.close();
        connection.close();
    }
}
2)消费者1
复制代码
package com.chen.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer_topics1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");

        //3.创建连接
        Connection connection=connectionFactory.newConnection();

        //4.创建频道
        Channel channel = connection.createChannel();

        //5.创建队列
        String queue1Name="test_topics_queue1";
        String queue2Name="test_topics_queue2";

        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
                System.out.println("将日志存入数据库");
            }
        };
        channel.basicConsume(queue1Name,true,consumer);
    }
}
3)消费者2
复制代码
package com.chen.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer_topics2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1.创建连接工厂
        ConnectionFactory connectionFactory=new ConnectionFactory();
        //2.配置参数
        //主机地址,根据你使用的rabbitmq为主,在本地就localhost,在阿里云就用其ip
        //默认为localhost
        connectionFactory.setHost("localhost");
        //连接端口,默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/itcast");
        //连接用户名;默认为quest
        connectionFactory.setUsername("chen");
        //连接密码;默认为guest
        connectionFactory.setPassword("a3344048");

        //3.创建连接
        Connection connection=connectionFactory.newConnection();

        //4.创建频道
        Channel channel = connection.createChannel();

        //5.创建队列
        String queue1Name="test_topics_queue1";
        String queue2Name="test_topics_queue2";

        //7.绑定队列和交换机
        //接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            /*
            回调方法,当收到消息后,会自动执行该方法
            1.consumerTag:标识
            2.envelope:获取一些消息,交换机,路由key...
            3.properties:配置消息
            4.body:数据
            * */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag:"+consumerTag);
                System.out.println("Exchange:"+envelope.getExchange());
                System.out.println("RoutingKey:"+envelope.getRoutingKey());
                System.out.println("properties:"+properties);
                System.out.println("body:"+new String(body));
                System.out.println("将日志打印控制台");
            }
        };
        channel.basicConsume(queue2Name,true,consumer);
    }
}
4.5.3 测试

启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看生产者发送对应routing key对应队列的消息;到达按照需要接收的效果;并且这些routing key可以使用通配符

在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchange选项卡,点击topic_exchange的交换机,可以查看如下的绑定:

4.5.4 小结

Topic主题模式可以实现Publish/Subscribe发布与订阅模式和Routing路由模式的功能:只是Topic在配置routing key的时候可以使用通配符,显得更加灵活。

4.6 模式总结

RabbitMQ工作模式:

  1. 简单模式 HelloWorld 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)

  2. 工作队列模式 Work Queue 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)

  3. 发布订阅模式 Publish/subscribe 需要设置类型为fanout的交换机 ,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列

  4. 路由模式 Routing 需要设置类型为direct的交换机 ,交换机和队列进行绑定,并且routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

  5. 通配符模式 Topic 需要设置类型为topic的交换机 ,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

5. Spring整合RabbitMQ

5.1 Spring整合RabbitMQ

需求:使用Spring整合RabbitMQ

步骤:

生产者

  1. 创建生产者工程

  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">
        <parent>
            <artifactId>chen-rabbitmq</artifactId>
            <groupId>com.chen</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>spring-rabbitmq-producer</artifactId>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.3.4</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.amqp</groupId>
                <artifactId>spring-rabbit</artifactId>
                <version>2.3.6</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.2</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.3.4</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    
    </project>
  3. 配置整合

    rabbitmq.properties

    复制代码
    rabbitmq.host=localhost
    rabbitmq.port=5672
    rabbitmq.username=chen
    rabbitmq.password=a3344048
    rabbitmq.virtual-host=/itcast

    spring-rabbitmq-producer.xml

    复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:rabbit="http://www.springframework.org/schema/rabbit"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/rabbit
           http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
    ">
        <!--加载配置文件-->
        <context:property-placeholder location="classpath:rabbitmq.properties"/>
        <!--定义rabbitmq connectionFactory-->
        <rabbit:connection-factory id="connectionFactory"
                                   host="${rabbitmq.host}"
                                   port="${rabbitmq.port}"
                                   username="${rabbitmq.username}"
                                   password="${rabbitmq.password}"
                                   virtual-host="${rabbitmq.virtual-host}"/>
        <!--定义管理交换机、队列-->
        <rabbit:admin connection-factory="connectionFactory"/>
    
        <!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机
        默认交换机类型为direct,名字为:"",路由键为队列的名称
        -->
        <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>
    
        <!--~~~~~~~广播:所有队列都能收到消息~~~~~~~~~~-->
        <!--定义广播交换机中的持久化队列,不存在则自动创建-->
        <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>
    
        <!--定义广播交换机中的持久化队列,不存在则自动创建-->
        <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>
    
        <!--定义广播类型交换机;并绑定上述两个队列-->
        <rabbit:fanout-exchange id="spring-fanout-exchange" name="spring-fanout-exchange" auto-declare="true">
            <rabbit:bindings>
                <rabbit:binding queue="spring_fanout_queue_1"/>
                <rabbit:binding queue="spring_fanout_queue_2"/>
            </rabbit:bindings>
        </rabbit:fanout-exchange>
    
        <!--~~~~~~~~~通配符:*匹配一个单词,#匹配多个单词~~~~~~~~~~~~-->
        <!--定义广播交换机中的持久化队列,不存在则自动创建-->
        <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
        <!--定义广播交换机中的持久化队列,不存在则自动创建-->
        <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
        <!--定义广播交换机中的持久化队列,不存在则自动创建-->
        <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>
    
        <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
            <rabbit:bindings>
                <rabbit:binding pattern="chen.*" queue="spring_topic_queue_star"/>
                <rabbit:binding pattern="chen.#" queue="spring_topic_queue_well"/>
                <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/>
            </rabbit:bindings>
        </rabbit:topic-exchange>
    
        <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
        <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
    </beans>
  4. 编写代码发送消息

    复制代码
    package com.chen.producer;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
    public class ProducerTest {
        //1.注入 RabbitTemplate
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testHelloWorld(){
            //发送消息
            rabbitTemplate.convertAndSend("spring_queue","hello world spring...");
        }
    
        /*发送fanout消息*/
        @Test
        public void testFanout(){
            //发送消息
            rabbitTemplate.convertAndSend("spring-fanout-exchange","","hello fanout...");
        }
    
        /*发送topic消息*/
        @Test
        public void testTopic(){
            //发送消息
            rabbitTemplate.convertAndSend("spring_topic_exchange","chen.hehe.haha","hello topic...");
        }
    }

消费者

  1. 创建消费者工程 spring-rabbitmq-consumer

  2. 添加依赖 与producer一样

  3. 配置整合

    复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:rabbit="http://www.springframework.org/schema/rabbit"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/rabbit
           http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
    ">
        <!--加载配置文件-->
        <context:property-placeholder location="classpath:rabbitmq.properties"/>
        <!--定义rabbitmq connectionFactory-->
        <rabbit:connection-factory id="connectionFactory"
                                   host="${rabbitmq.host}"
                                   port="${rabbitmq.port}"
                                   username="${rabbitmq.username}"
                                   password="${rabbitmq.password}"
                                   virtual-host="${rabbitmq.virtual-host}"/>
    
        <bean id="springQueueListener" class="com.chen.rabbitmq.listener.SpringQueueListener"/>
        <bean id="fanoutListener1" class="com.chen.rabbitmq.listener.fanoutListener1"/>
        <bean id="fanoutListener2" class="com.chen.rabbitmq.listener.fanoutListener2"/>
        <bean id="topicListenerStar" class="com.chen.rabbitmq.listener.topicListenerStar"/>
        <bean id="topicListenerWell" class="com.chen.rabbitmq.listener.topicListenerWell"/>
        <bean id="topicListenerWell2" class="com.chen.rabbitmq.listener.topicListenerWell2"/>
    
        <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
            <rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
            <rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
            <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
            <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
            <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
            <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>
        </rabbit:listener-container>
       </beans>
  4. 编写消息监听器

    复制代码
    package com.chen.rabbitmq.listener;
    
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessageListener;
    
    public class SpringQueueListener implements MessageListener {
        @Override
        public void onMessage(Message message) {
            //打印消息
            System.out.println(new String(message.getBody()));
        }
    }
  5. 测试

    复制代码
    package com.chen;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
    public class ConsumerTest {
        @Test
        public void test1(){
            boolean flag=true;
            while(true){}
        }
    }

6. SpringBoot整合RabbitMQ

5.1 简介

在Spring项目中,可以使用Spring-Rabbit去操作RabbitMQ

尤其是在spring boot项目中只需要引入对应的amqp启动器依赖即可,方便的使用RabbitTemplate发送消息,使用注解接收消息。

一般在开发过程中:

生产者工程:

1.application.yml文件配置RabbitMQ相关信息

2.在生产者工程中编写配置类,用于创建交换机和队列,并进行绑定

3.注入RabbitTemplate对象,通过RabbitTemplate对象发送消息到交换机

消费者工程:

1.application.yml文件配置RabbitMQ相关信息

2.创建消息处理类,用于接收队列中的消息并进行处理

5.2 搭建生产者工程

5.2.1 创建工程

创建生产者工程springboot-rabbitmq-producer

5.2.2 添加依赖

修改pom.xml文件内容为如下:

复制代码
<?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>

    <artifactId>producer-springboot</artifactId>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
    </parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
        <version>2.4.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>2.4.3</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
</project>
5.2.3 启动类
复制代码
package com.chen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProducerApplication.class);
    }
}
5.2.4 配置RabbitMQ
  1. 配置文件

    创建application.yml,内容如下:

    复制代码
    # 配置RabbitMQ的基本信息 ip 端口 username password
    spring:
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
        virtual-host: /
  2. 绑定交换机和队列

    创建RabbitMQ队列与交换机绑定的配置类com.chen.rabbitq.config.RabbitMQConfig

    复制代码
    package com.chen.rabbitmq.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RabbitMQConfig {
        public static final String EXCHANGE_NAME="boot_topic_exchange";
        public static final String QUEUE_NAME="boot_queue";
    
        //1.交换机
        @Bean("bootExchange")
        public Exchange bootExchange(){
            return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
        }
    
        //2.Queue队列
        @Bean("bootQueue")
        public Queue bootQueue(){
            return QueueBuilder.durable(QUEUE_NAME).build();
        }
    
        //3.队列和交换机绑定关系 Binding
        /*
        1.知道哪个队列
        2.知道哪个交换机
        3.routing key
        * */
        @Bean
        public Binding bingQueueExchange(@Qualifier("bootQueue")Queue queue,@Qualifier("bootExchange")Exchange exchange){
            return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
        }
    }
  3. 测试

    复制代码
    package com.chen.test;
    
    import com.chen.rabbitmq.config.RabbitMQConfig;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class ProducerTest {
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSend(){
            rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haha","boot mq hello");
        }
    }

5.3 搭建消费者工程

5.3.1 创建工程
5.3.2 添加依赖 与producer一样
5.3.3 启动类
复制代码
package com.chen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class);
    }
}
5.3.4 配置RabbitMQ

创建application.yml,与producer的配置一致

5.3.5 消息监听处理类
复制代码
package com.chen.rabbitmq.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class RabbitMQListener {

    @RabbitListener(queues = "boot_queue")
    public void ListenerQueue(Message message){
        System.out.println(message);
    }
}

5.4 测试

启动 启动类,查看

小结
  • SpringBoot提供了快速整合RabbitMQ的方式

  • 基本信息在yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置

  • 生产端之间注入RabbitTemplate完成消息发送

  • 消费端直接使用@RabbitListener完成消息接收

二、第二天

1.RabbitMQ内容介绍

RabbitMQ高级特性

  • 消息可靠性投递

  • Consumer ACK

  • 消费端限流

  • TTL

  • 死信队列

  • 延迟队列

  • 日志与监控

  • 消息可靠性分析与追踪

  • 管理

RabbitMQ应用问题

  • 消息可靠性保障

  • 消息幂等性处理

RabbitMQ集群搭建

  • RabbitMQ高可用集群

2. RabbitMQ 高级特性

2.1 消息的可靠投递

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

  • confirm 确认模式

  • return 退回模式

rabbitmq整个消息投递的路径为:

producer-->rabbitmq broker-->exchange-->queue-->consumer

  • 消息从producer到exchange 则会返回一个confirmCallback

  • 消息从exchange-->queue投递失败 则会返回一个returnCallback

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

2.2 消息可靠性投递 confirm模式

1.创建rabbit-producer-spring
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">
    <parent>
        <artifactId>chen-rabbitmq</artifactId>
        <groupId>com.chen</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rabbitmq-producer-spring</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.3.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.4</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
3.配置参数

rabbitmq.properties

复制代码
rabbitmq.host=localhost
rabbitmq.port=5672
rabbitmq.username=chen
rabbitmq.password=a3344048
rabbitmq.virtual-host=/itcast

spring-rabbitmq-producer.xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd

">

    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq connectionFaxtory-->
    <rabbit:connection-factory id="connectionFactory"
                               host="localhost"
                               port="5672"
                               username="guest"
                               password="guest"
                               virtual-host="/"
                               publisher-returns="true"
    />
    <!--定义管理交换机、队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--消息可靠性投递(生产端)-->
    <rabbit:queue id="test_queue_confirm" name="test_queue_confirm" auto-declare="true"/>
    <rabbit:direct-exchange name="test_exchange_confirm">
        <rabbit:bindings>
            <rabbit:binding queue="test_queue_confirm" key="confirm"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
4.测试
复制代码
package com.chen.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /*
    * 确认模式:
    *  步骤:
    *   1.确认模式开启:ConnectionFactory中开启publisher-confirm(rabbitmq旧版本) 而新版本是confirm-type 有三种模式
    *      NONE值是禁用发布确认模式,是默认值  就是旧版本的false
    *      CORRELATED值是发布消息成功到交换器后会触发回调方法
    *      SIMPLE值经测试有两种效果:
    *        1.效果和CORRELATED值一样会触发回调方法
    *        2.在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,
    *          要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
    *   2.在rabbitTemplate定义ConfirmCallBack回调函数
    * */
    @Test
    public void testConfirm(){
        //2.定义回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *
             * @param correlationData 相关配置信息
             * @param b exchange交换机 是否成功收到了消息,true 成功,false代表失败
             * @param s 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("confirm方法被执行了");
                if (b){
                    //接收成功
                    System.out.println("接收成功消息"+s);
                }else {
                    //接收失败
                    System.out.println("接收失败消息"+s);
                }
            }
        });
        //3.发送消息
        rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message confirm...");
    }
}

2.3 消息投递 退回模式return

复制代码
/*
    * 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行ReturnCallBack
    * 步骤:
    * 1.开启回退模式:publisher-returns="true"
    * 2.设置ReturnCallBack
    * 3.设置Exchange处理消息的模式:
    *  1.如果消息没有路由到Queue,则丢弃消息(默认)
    *  2.如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
    * */
    @Test
    public void testReturn(){
        //一般发送信息错误,退回信息模式会直接丢失信息 所以我们要设置模式
        //设置交换机处理失败消息的模式
        rabbitTemplate.setMandatory(true);
        //2.设置ReturnCallBack
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             * 旧版:
             * @param message 消息对象
             * @param replyCode 错误码
             * @param replyText 错误信息
             * @param exchange 交换机
             * @param routingKey 路由键
             */
            /*@Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

            }*/

            /**
             * 新版
             * @param returnedMessage 相对于旧版而言 这个returnedMessage包含了旧版的所有信息 直接get就可以得到了
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("return 执行。。。");
                System.out.println(returnedMessage.getMessage());
                System.out.println(returnedMessage.getReplyCode());
                System.out.println(returnedMessage.getReplyText());
                System.out.println(returnedMessage.getExchange());
                System.out.println(returnedMessage.getRoutingKey());
            }
        });
        //3.发送消息
        rabbitTemplate.convertAndSend("test_exchange_confirm","confirm","message confirm...");
    }

2.4 消息的可靠投递小结

  • 设置ConnectionFactory的confirm-type的模式

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

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

  • 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer,并执行回调函数returnedMessage

  • 在RabbitMQ中也提供了事务机制,但是性能较差,此处不做讲解。

    使用channel下列方法,完成事务控制:

    • txSelect(),用于将当前channel设置成transaction模式

    • txCommit(),用于提交事务

    • txRollback(),用于回滚事务

3.1 Consumer Ack

ack指Acknowledge,确认。表示消费端收到消息后的确认方式。

有三种确认方式:

  • 自动确认:acknowledge="none"

  • 手动确认:acknowledge="manual"

  • 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

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

1.配置环境
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">

    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq的connectionFactory-->
    <rabbit:connection-factory id="connectionFactory"
                               host="localhost"
                               port="5672"
                               username="guest"
                               password="guest"
                               virtual-host="/"/>

    <context:component-scan base-package="com.chen.listener"/>
    <!--定义监听器bean-->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>
    </rabbit:listener-container>
</beans>
2.监听器
复制代码
package com.chen.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/*
* Consumer ACK机制:
* 1.设置手动签收。acknowledge="manual"
* 2.让监听类实现ChannelAwareMessageListener接口
* 3.如果消息成功处理,则调用channel的basicAck()签收
* 4.如果消息处理失败,则调用channel的basicNack()签收,broker重新发送给consumer
* */
@Component
public class AckListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2.处理业务逻辑
            System.out.println("处理业务逻辑");

            //3.手动签收
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            //4.拒绝签收
            /*
            * 第三个参数:requeue:重回队列,如果设置为true,则消息重新回到queue,broker会重新回到queue,broker会重新发送该消息给消费端
            * */
            channel.basicNack(deliveryTag,true,true);
        }
    }
}
3.测试
复制代码
package com.chen.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class ConsumerTest {
    @Test
    public void test(){
        while (true){

        }
    }
}

3.2 Consumer Ack 小结

  • 在rabbit:listener-container标签中设置acknowledge属性,设置ack方式none:自动确认,manual:手动确认

  • 如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag.false);方法确认签收消息

  • 如果出现异常,则在catch中调用basicNack或basicReject,拒绝消息,让MQ重新发送消息。

总结

  1. 持久化

    • exchange要持久化

    • queue要持久化

    • message要持久化 也就是设declare为true

  2. 生产方确认Confirm

  3. 消费方确认Ack

  4. Broker高可用

4.1 消费端限流

也就是对该队列的消息进行限流

1.配置环境
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">

    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq的connectionFactory-->
    <rabbit:connection-factory id="connectionFactory"
                               host="localhost"
                               port="5672"
                               username="guest"
                               password="guest"
                               virtual-host="/"/>

    <context:component-scan base-package="com.chen.listener"/>
    <!--定义监听器bean-->
    <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
        <!--<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>-->
        <rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>
    </rabbit:listener-container>
</beans>
2.监听器
复制代码
package com.chen.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/*
* Consumer 限流机制:
* 1.确保ack机制为手动确认
* 2.listener-container配置属性
*   perfetch=1,表示消费端每次从mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉去下一条消息
* */
@Component
public class QosListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        //1.获取消息
        System.out.println(new String(message.getBody()));

        //2.处理业务逻辑

        //3.签收
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);

    }
}
小结
  • 在<rabbit:listener-container >中配置prefetch属性设置消费端一次拉去多少消息

  • 消费端的确认模式一定为手动确认。acknowledge="manual"

5.1 ttl

ttl过期时间设置有:1.队列统一过期 2.消息单独过期

1.队列统一过期
  • 配置环境

    复制代码
    <!--ttl-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <!--设置queue的参数 指queue的过期时间-->
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
    
    <rabbit:topic-exchange name="test_exchange_ttl" id="test_exchange_ttl">
        <rabbit:bindings>
            <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    注意:过期时间的value-type要设置成数字,其默认为字符串,不设置会报错!!!

  • 测试

    复制代码
    /*
        * TTL:过期时间
        * 1.队列统一过期
        *
        * 2.消息单独过期
        *
        * */
        @Test
        public void testTtl(){
            for (int i = 0; i < 10; i++) {
                //发送消息
                rabbitTemplate.convertAndSend("test_exchange_ttl","ttl.hehe","message ttl...");
            }
        }

    在rabbitmq网页可知道10秒过消息自动消失,这个是通过队列的过期时间进行设置的。

2.消息单独过期

相对于上述对队列进行过期时间,对消息进行单独过期更有利于方便操作

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

}

如果设置了消息的过期时间,也设置了队列的过期时间,按时间短为准

队列过期后,会将队列所有消息全部移除

消息过期后,只有消息在队列顶端才会判断其是否过期(移除掉) 也就是在消费的时候才会判断

下列代码运行可知,当消息过了5秒,已经过期的消息依然没有被删除,而是要消费者接收消息时,才会进行删除,和redis的惰性删除是一样的用法!

复制代码
@Test
public void testTtl3(){
    //消息后处理对象,设置一些消息的参数信息
    MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            //1.设置message的信息
            message.getMessageProperties().setExpiration("5000");//消息过期时间
            //2.返回该消息
            return message;
        }
    };
    for (int i = 0; i < 10; i++) {
        if (i==5){
            rabbitTemplate.convertAndSend("test_exchange_ttl","ttl.hehe","message ttl...",messagePostProcessor);
        }else {
            rabbitTemplate.convertAndSend("test_exchange_ttl","ttl.hehe","message ttl...");
        }
    }
3.TTL小结
  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。

  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期

  • 如果两者都进行了设置,以时间短的为准

6.1 死信队列

含义:

死信队列,英文缩写:DLX。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

问题:1.该队列如何绑定DLX?2.如何判断消息为死信呢?

消息成为死信的三种情况:

  1. 队列消息长度到达限制

  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;

  3. 原队列存在消息过期设置,消息到达超时间未被消费;

队列绑定死信交换机:

给队列设置参数:x-dead-letter-exchange 和 x-dead-letter-routing-key

代码实现:
1.过期时间还没有接收的死信
复制代码
<!--
1.声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)
-->
<rabbit:queue id="test_queue_dlx" name="test_queue_dlx">
    <!--3.正常队列绑定死信交换机-->
    <rabbit:queue-arguments>
        <!--3.1 x-dead-letter-exchange:死信交换机名称-->
        <entry key="x-dead-letter-exchange" value="exchange_dlx"/>
        <!--3.2 x-dead-letter-routing-key:发送给死信交换机的routingkey-->
        <entry key="x-dead-letter-routing-key" value="dlx.hehe"/>
        <!--4.1 设置队列的过期时间 ttl-->
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        <!--4.2 设置队列的长度限制 max-length -->
        <entry key="x-max-length" value="10" value-type="java.lang.Integer"/>
    </rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="test_exchange_dlx">
    <rabbit:bindings>
        <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>

<!--
2.声明死信队列(queue_dlx)和死信交换机(exchange_dlx)
-->
<rabbit:queue id="queue_dlx" name="queue_dlx"></rabbit:queue>
<rabbit:topic-exchange name="exchange_dlx">
    <rabbit:bindings>
        <rabbit:binding pattern="dlx.#" queue="queue_dlx"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>

测试代码:

复制代码
@Test
public void testDlx(){
    //1.测试过期时间,死信消息
    rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","dead message?");
}
2.测试长度限制后,消息死信
复制代码
for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","dead message?");
        }
3.消息拒收
复制代码
<!--定义监听器,监听正常队列-->
<rabbit:listener ref="dlxListener" queue-names="test_queue_dlx"/>
复制代码
package com.chen.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

@Component
public class DlxListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2.处理业务逻辑
            System.out.println("处理业务逻辑");
            int i=3/0;
            //3.手动签收
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            //4.拒绝签收
            /*
            * 第三个参数:requeue:重回队列,如果设置为true,则消息重新回到queue,broker会重新回到queue,broker会重新发送该消息给消费端
            * 不重回队列  第三个参数:requeue:重回队列,如果设置为false
            * */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}
复制代码
//3.消息拒收
rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","dead message?");
小结:
  1. 死信交换机和死信队列 跟普通的没有区别

  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

  3. 消息成为死信的三种情况:

    • 队列消息长度到达限制

    • 消费者拒接消费消息,并且不重回队列

    • 原队列存在消息过期设置,消息到达超时时间未被消费

7.1 延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存

  2. 新用户注册成功7天后,发送短信问候

实现方式:

  1. 定时器

  2. 延迟队列

RabbitMQ中并未提供延迟队列功能。

但是可以使用:TTL+死信队列 组合实现延迟队列的效果

代码实现:
1.生产者
复制代码
<!--
延迟队列:
1.定义正常交换机(order_exchange)和队列(order_queue)
2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
3.绑定,设置正常队列过期时间为30分钟
-->

<!--1.定义正常交换机(order_exchange)和队列(order_queue)-->
<rabbit:queue id="order_queue" name="order_queue">
    <rabbit:queue-arguments>
        <entry key="x-dead-letter-exchange" value="order_exchange_dlx"/>
        <entry key="x-dead-letter-routing-key" value="dlx.order.cancel"/>
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
    </rabbit:queue-arguments>
</rabbit:queue>

<rabbit:topic-exchange name="order_exchange">
    <rabbit:bindings>
        <rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>

<!--2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)-->
<rabbit:queue id="order_queue_dlx" name="order_queue_dlx"></rabbit:queue>
<rabbit:topic-exchange name="order_exchange_dlx">
    <rabbit:bindings>
        <rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>

在测试之前,先测试之前的方法创建,然后再测试

复制代码
@Test
public void testDelay() throws InterruptedException {
    //1.发送订单消息。将来是订单系统中,下单成功后,发送消息
    rabbitTemplate.convertAndSend("order_exchange","order.msg","订单信息:id=1");

    //2.打印倒计时10秒
    for (int i=10;i>0;i--){
        System.out.println(i+"...");
        Thread.sleep(1000);
    }
}
2.消费者
复制代码
package com.chen.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

@Component
public class OrderListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2.处理业务逻辑
            System.out.println("处理业务逻辑");
            System.out.println("根据订单id查询其状态");
            System.out.println("判断状态是否为支付成功");
            System.out.println("取消订单,回滚库存...");
            //3.手动签收
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            //4.拒绝签收
            /*
            * 第三个参数:requeue:重回队列,如果设置为true,则消息重新回到queue,broker会重新回到queue,broker会重新发送该消息给消费端
            * 不重回队列  第三个参数:requeue:重回队列,如果设置为false
            * */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}
复制代码
<!--延迟队列效果实现:一定要监听的是 死信队列!!!-->
<rabbit:listener ref="orderListener" queue-names="order_queue_dlx"/>
复制代码
package com.chen.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class ConsumerTest {
    @Test
    public void test(){
        while (true){

        }
    }
}

要先执行消费者的启动类,才能观察的出队列消息的变化!

注意:当配置配错时,要先删除配置的队列或者交换机,重新进行配置!

7.2 延迟队列小结

1.延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。

2.RabbitMQ没有提供延迟队列功能,但是可以使用:TTL+DLX来实现延迟队列效果。

8.1日志与监控

8.1.1 RabbitMQ 日志

RabbitMQ默认日志存放路径:/var/log/rabbitmq/rabbit@xxx.log

日志包含了RabbitMQ的版本号、Erlang的版本号、RabbitMQ服务节点名称、cookie的hash值、RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建以及权限配置等等

8.1.2 web管控台监控
8.1.3 rabbitmqctl管理和监控

注意:这些命令行,在linux系统中是在rabbitmq包进行命令行,而win需要在管理员的cmd,在rabbitmq包中的sbin进行命令行

  • 查看队列

    rabbitmqctl list_queues

  • 查看exchanges

    rabbitmqctl list_exchange

  • 查看用户

    rabbitmqctl list_users

  • 查看连接

    rabbitmqctl list_connections

  • 查看消费者信息

    rabbitmqctl list_consumers

  • 查看环境变量

    rabbitmqctl environment

  • 查看未被确认的队列

    rabbitmqctl list_queues name messages_unacknowledged

  • 查看单个队列的内存使用

    rabbitmqctl list_queues name memory

  • 查看准备就绪的队列

    rabbitmqctl list_queues name messages_ready

9.1消息追踪

在使用任何消息中间件的过程中,难免会出现某条消息异常丢失 的情况。对于RabbitMQ而言,可能是因为生产者或消费者与RabbitMQ断开了连接 ,而它们与RabbitMQ又采用了不同的确认机制 ;也有可能是因为交换器与队列之间不同的转发策略 ;甚至是交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相应的措施;另外RabbitMQ本身的集群策略也可能导致消息的丢失。这个时候就需要有一个较好的机制跟踪记录消息的投递过程,以此协助开发和运维人员进行问题的定位。

在RabbitMQ中可以使用Firehose和rabbitmq_tracing插件功能来实现消息追踪。

9.1.1 消息追踪-firehose

firehose的机制是将生产者投递给rabbitmq的消息 ,rabbitmq投递给消费者的消息按照指定的格式发送到默认的exchange上。这个默认的exchange 的名称为amq.rabbitmq.trace ,它是一个topic类型的exchange 。其中exchangename和queuename为实际exchange和queue的名称,分别对应生产者投递到exchange的消息和消费者从queue获取的消息

注意:打开trace会影响消息写入功能,适当打开后请关闭。

在linux版本输入命令行:

rabbitmqctl trace_on:开启Firehose命令

rabbitmqctl trace_off:关闭Firehose命令

也就是说,当我们发送消息时,默认的交换机还会发送一个更加详细的消息到queue

9.1.2 消息追踪-rabbitmq_tracing

rabbitmq_tracing和Firehose在实现上如出一致,只不过rabbitmq_tracing的方式比Firehose多了一层GUI的包装,更容易使用和管理。

启用插件:rabbitmq-plugins enable rabbitmq_tracing

只接收发过来的就在pattern填publish.# 想看接送的就deliver.#

3.RabbitMQ 应用问题

RabbitMQ应用问题

  1. 消息可靠性保障

    • 消息补偿机制
  2. 消息幂等性保障

    • 乐观解决方案

3.1消息可靠性保障--消息补偿

  1. Producer先进行入库,发送消息Q1,Consumer进行监听,保存到DB中--正常情况

  2. 当Producer数据入库,但消息发送失败,consumer得不到消息也会出错

  3. 所以Producer会延迟发送消息,消息和q1队列的消息是一致的,被consumer收到发送确认消息,回调检查服务监听确认消息和延迟消息进行对比是否一致

    • 如果一致,说明q1发送消息成功,已经被消费了

    • 如果没有,则说明q1发送消息失败,那我们要进行调用Producer重新发送消息

  4. 当延迟消息和发送的消息都失败了怎么办?我们会把回调服务的MDB和Producer的DB数据比对

3.2 消息幂等性保障

幂等性值一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所资源本身所产生的影响均与一次执行的影响相同。

在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

例子:当你付钱,由于网络问题,后台有两条500扣款,但是你只是付一次500,那是扣500还是1000

3.2.1 消息幂等性保障--乐观锁机制
  1. Producer发送id=1 money=500 version=1的数据

    由于可能consumer挂了的原因,consumer不能接收消息,这是就会再发送多一条消息(消息退回),此时q1队列有两条一样的数据发送到consumer,consumer获得两条一样的数据

4. RabbitMQ集群搭建

摘要:实际生产应用中都会采用消息队列的集群方案,如果选择RabbitMQ那么有必要了解下它的集群方案原理

一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境上使用其单实例一般都会考虑使用RabbitMQ的集群方案。

4.1集群方案的原理

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写 ,Erlang语言天生具备分布式特性 (通过同步Erlang集群各节点的magic cookie 来实现)。因此,RabbitMQ天然支持Clustering 。这使得RabbitMQ本身不需要像ActiveMQ、Dafka那样通过Zookeeper 分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐能力的目的。

4.2 单机多实例部署

由于某些因素的限制,有时候你不得不在一台机器上去搭建一个rabbitmq集群,这个有点类似zookeeper的单机版,真实生成环境还是要配成多机集群的。有关怎么配置多机集群的可以参考其它的资料,这里主要论述如何在单机中配置多个rabbitmq实例。

主要参考官方文档:Clustering Guide | RabbitMQ

首先确保RabbitMQ运行没有问题

  • 停止rabbitmq服务

    linux版本:

    复制代码
    service rabbitmq-server stop

    win版本:

    复制代码
    rabbitmq-service stop
  • 启动第一个节点

    linux版本:

    复制代码
    RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start
  • 启动第二个节点

    web管理插件端口占用,所以还要指定其web插件占用的端口号

    复制代码
    RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port.15673}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start
  • 结束命令:

    复制代码
    rabbitmqctl -n rabbit1 stop
    rabbitmqctl -n rabbit2 stop
  • rabbit1操作作为主节点:

    复制代码
    # rabbitmqctl -n rabbit1 stop_app
    
    # rabbitmqctl -n rabbit1 reset
    
    # rabbitmqctl -n rabbit1 start_app
  • rabbit2操作为从节点

    复制代码
    # rabbitmqctl -n rabbit2 stop_app
    
    # rabbitmqctl -n rabbit2 reset  ### 重置
    
    # rabbitmqctl -n rabbit2 join_cluster rabbit1@'super'  ###''内是主机名缓存自己的
    
    # rabbitmqctl -n rabbit2 start_app
  • 查看集群状态

    复制代码
    rabbitmqctl cluster_status -n rabbit1

但是当从节点上传数据时,主节点挂机后,数据就访问不了了,这个就是一个简单集群的问题。

4.3 集群管理

  • rabbitmqctl join_cluster {cluster_node}{-ram}

    将节点加入指定集群中,在这个命令行执行前需要停止RabbitMQ应用并重置节点。

  • rabbitmqctl cluster_status

    显示集群状态

  • rabbitmqctl change_cluster_node_type {disc|ram}

    修改集群节点的类型。在这个命令执行前需要停止RabbitMQ应用

  • rabbitmqctl forget_cluster_node -offline

    将节点从集群中删除,允许离线执行

  • rabbitmqctl update_cluster_nodes {clusternode}

    在集群中的节点应用启动前咨询clusternode节点的最新信息,并更新相应的集群信息。这个和join_cluster不同,它不加入集群。考虑这样一种情况,节点A和节点B都在集群中,当节点A离线了,节点C又和节点B组成了一个集群,如何节点B又离开了集群,当A醒来的时候,它会尝试联系节点B,但是这样会失败,因为节点B已经不在集群中了。

  • rabbitmqctl cancel_sync_queue -p vhost{queue}

    取消队列queue同步镜像的操作

  • rabbitmqctl set_cluster_name {name}

    设置集群名称。集群名称在客户端连接时会通报给客户端。Federation和Shove插件也会有用到集群名称的地方。集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置。

4.4 RabbitMQ镜像集群配置

上面已经完成RabbitMQ默认集群模式,但并不保证队列的高可用性,尽管交换机、绑定这些可以复制到集群里的任何一个节点,但是队列内容不会复制。虽然该模式解决一项目组节点压力,但队列节点宕机直接导致该队列无法应用,只能等待重启,所以要像在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,必须要创建镜像队列
镜像队列是基于普通的集群模式的,然后再添加一些策略,所以你还是得先配置普通集群,然后才能设置镜像队列,我们就以上面得集群接着做。

设置的镜像队列可以通过开启的网页的管理端Admin->Policies,也可以通过命令。

rabbitmqctl set_policy my_ha "^" '{"ha-mode":"all"}'

4.5 负载均衡-HAProxy

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter、Reddit、StackOverflow、GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。

4.5.1 安装HAProxy
复制代码
//下载依赖包
yum install gcc vim wget
//上传haproxy源码包
//解压
tar -zxvf haproxy-1.6.5.tar.gz -C /usr/local
//进入目录、进行编译、安装
cd /usr/local/haproxy-1.6.5
make TARGET=linux31 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy
mkdir /etc/haproxy
//赋权
groupadd -r -g 149 haproxy
useradd -g haproxy -r -s /sbin/nologin -u 149 haproxy
//创建haproxy配置文件
mkdir /etc/haproxy
vim /etc/haproxy/haproxy.cfg
3.5.2 配置HAProxy

配置文件路径:/etc/haproxy/haproxy.cfg

复制代码
#logging opt options
global
     log 127.0.0.1 local0 info
     maxconn 5120
     chroot /usr/local/haproxy
     uid 99
     gid 99
     daemon 20
     pidfile /var/run/haproxy.pid
     
defaults
     log global
     
     mode tcp
     
     option tcplog
     option dontlognull
     retries 3
     option redispatch
     maxconn 2000
     
      clitimeout 60s
      
      srvtimeout 15s
#front-end IP for consumers and producters

listen  rabbitmq_cluster
     bind 0.0.0.0:5672
     
     mode tcp
     #balance url_param userid
     #balance url_param session_id check_post 64
     #balance hdr(User-Agent)
     #balance hdr(host)
     #balance hdr(Host) use_domain_only
     #balance rdp-cookie
     #balance source //ip
     
     balance roundrobin
     
         server nodel 127.0.0.1:5673 check inter 5000 rise 2 fall 2
         server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2
         
listen stats
     bind 172.16.98.133:8100
     mode http
     option httplog
     stats enable
     stats uri/rabbitmq-stats
     stats refresh 5s

listen rabbitmq_cluster bind 0.0.0.0:5672

HAproxy对外使用的接口

bind 172.16.98.133:8100

stats uri/rabbitmq-stats

通过这个来访问管理的控制台

启动HAproxy负载

复制代码
/usr/local/haproxy/sbin/haproxy -f/etc/haproxy/haproxy.cfg
//查看haproxy进程状态
ps -ef | grep haproxy

访问如下地址对mq节点进行监控
http://172.16.98.133:8100/rabbitmq-stats

代码中访问mq集群地址,则编为访问haproxy地址:5672

相关推荐
敖行客 Allthinker2 小时前
分布式远程研发团队,借助云 IDE 统一开发环境
ide·分布式
磐链科技12 小时前
交场景下的钱包开发痛点:并发交易与消息推送的分布式一致性技术方案
分布式
程序员与背包客_CoderZ18 小时前
高性能分布式KV存储引擎RocksDB入门与C/C++编码实战
c语言·开发语言·数据库·c++·分布式·分布式数据库·rocksdb
汽车仪器仪表相关领域20 小时前
ZDT‑I伺服电机测试系统:四象限动态加载
大数据·数据库·分布式·功能测试·汽车·压力测试·可用性测试
骇客野人21 小时前
Java分布式任务调度方案(完整落地指南)
java·开发语言·分布式
每天一道题1 天前
从 async/await 到幂等恢复:把 Python 并发和 Agent Runtime 一次讲清楚
分布式·python
Zach_菠萝侠1 天前
【deepseek harness研究】进化方向7:分布式与远程执行 思考、设计与实现
分布式·深度学习·deepseek
Sayai1 天前
Kafka 集群开启 SASL 认证实战(二):外置 ZooKeeper 之 kafka 与 zk 间认证
分布式·zookeeper·kafka
Sayai1 天前
Kafka 集群开启 SASL 认证实战(一):内置 ZooKeeper 场景,JAAS 配置全流程
分布式·zookeeper·kafka