SpringBoot:整合RabbitMQ(基本概念,消息模型,AmqpTemplate )

本文深入讲解RabbitMQ的原理及应用实践,涵盖消息中间件基础知识、RabbitMQ安装配置、内部结构与工作机制等内容,并通过实例演示如何利用RabbitMQ实现各种消息模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

消息中间件在互联网公司使用的越来越多,主要用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。消息队列实现系统之间的双向解耦,生产者往消息队列中发送消息,消费者从队列中拿取消息并处理,生产者不用关心是谁来消费,消费者不用关心谁在生产消息,从而达到系统解耦的目的,也大大提高了系统的高可用性和高并发能力。

1,基本概念 

1.1,JMS和AMQP

JMS是Java Message Service的缩写,即Java消息服务。JMS是Java EE技术规范中的一个重要组成部分,它是一种企业消息机制的规范。JMS就像一个只能交换机,负责路由分布式应用中各组件所发出的消息。

JMS提供了一组通用的Java应用程序接口(API),开发者可以通过这组通用的API来创建、发送、接收、读取消息。JMS是一种与具体实现厂商无关的API,它的作用类似于JDBC:不管底层采用何种数据库系统,应用程序总是面向通用的JDBC API编程。类似地,JMS则提供了与各厂商无关的API,不管底层采用何种消息服务实现,应用程序总是面向通用的JMS API编程。借助于JMS,开发者无须为使用A消息服务器产品先学习一次,为使用B消息服务器产品又要重新学习一次。

JMS提供了一组基本的API来操作消息系统:

  • ConnectionFactory(连接工厂):JMS客户端使用连接工厂创建JMS连接。
  • Connection(连接):表示客户端与服务器之间的活动连接。JMS客户端通过连接工厂创建连接。
  • Session(会话):表示客户端与JMS服务器之间的通信状态。JMS会话建立在连接之上,表示JMS客户端与服务器之间的通信线程。会话定义了消息的顺序,JMS使用会话进行事务性的消息处理。
  • Destination(消息目的):即消息生产者发送消息的目的地,也是消费者获取消息的消息源。
  • MessageProducer(消息生产者):消息生产者负责创建消息并将消息发送到消息目的地。
  • MessageConsumer(消息消费者):消息消费者负责接收消息并读取消息内容。

高级消息队列协议(Advanced Message Queuing Protocol,AMQP)是一种与平台无关的、线路级(wire-level)的消息中间件协议。AMQP并不属于JMS范畴,AMQP和JMS的区别与联系如下:

  • JMS定义了消息中间件的规范,从而实现对消息操作的统一;AMQP则通过制定协议来统一数据交互的格式。
  • JMS限定了必须使用Java语言;AMQP只制定协议,不规定实现语言和实现方式,因此是跨语言的。
  • JMS只制定了两种消息模型;而AMQP的消息模型更加灵活。
  • AMQP中增加了Exchange和Binging角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收;而Binding决定Exchange的消息应该发送到哪个队列。

RabbitMQ是典型的AMQP产品,它是用Erlang语言开发的。从灵活性的角度来看,RabbitMQ比ActiveMQ更加优秀;从性能上看,RabbitMQ完胜ActiveMQ。因此,很多公司都会优先选择RabbitMQ作为消息队列。

RabbitMQ基于开源的AMQP协议实现,服务器端用Erlang语言编写,支持多种客户端,如Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP、AJAX等。

  • 可靠性:使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。
  • 灵活的路由:在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,既可以将多个Exchange绑定在一起,又可以通过插件机制实现自己的Exchange。
  • 消息集群:多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
  • 高可用:队列可以在集群中的机器上进行镜像,使得在部分节点出现问题的情况下队列仍然可用。
  • 多种协议:支持多种消息队列协议,如STOMP、MQTT等。
  • 多种语言客户端:几乎支持所有常用语言,比如Java、.NET、Ruby等。
  • 管理界面:提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。

RabbitMQ与JMS规范的架构区别: JMS规范中的消息生产者和消费者都是直接与消息目的耦合的,消息生产者向消息目的发送消息,消息消费者从消息目的读取消息;而RabbitMQ则增加了Exchange的概念,通过Exchange对消息生产者与消息消费者做了进一步隔离——消息生产者向Exchange发送消息,消息消费者从消息队列读取消息,Exchange则负责将消息分发到各消息队列。

1.2,安装和配置RabbitMQ

由于RabbitMQ是采用Erlang语言编写的,因此运行RabbitMQ必须要有Erlang环境:

apt-get install erlang-nox

有了Erlang环境后,可以安装RabbitMQ:

apt install rabbitmq-server

启动rabbitmq_management插件:

rabbitmq-plugins enable rabbitmq_management

启动,关闭RabbitMQ服务:

service rabbitmq-server restart

service rabbitmq-server stop

添加用户:

#添加用户
rabbitmqctl add_user root 123456
#设置管理员,只有管理员才能远程登录
rabbitmqctl set_user_tags root administrator

访问“http://127.0.0.1:15672/”,将看到登录页面,输入账号密码登录RabbitMQ的Web管理界面。

1.3,用户和虚拟主机管理

点击界面上的Admin标签可以进入系统管理标签页:

在右边可以看到Users、Virtual Hosts等标签,点击可以进入用户管理、虚拟主机管理等界面。在下方可以通过Add user添加用户,添加用户时需要指定用户名、密码和Tags,Tags代表该用户的标签,主要是给程序员看的,知道用户大概有什么作用。

  • administrator:超级管理员,可登录管理控制台,查看所有信息,并且可以对用户、策略(policy)进行管理。
  • monitoring:监控者,可登录管理控制台,同时可以查看RabbitMQ节点的相关信息(进程数、内存使用情况、磁盘使用情况等)。
  • policymaker:策略制定者,可登录管理控制台,也可以对策略进行管理,但无法查看节点的相关信息。
  • management:普通管理者,可登录管理控制台,但无法查看节点信息,也无法对策略进行管理。
  • 其他:无法登录管理控制台,通常就是指普通的生产者和消费者。

这里添加一个用户:root,将其密码设置为123456,将其Tags设置为administrator(单击Tags右边灰色区域内的Admin链接,就会自动向Tags文本框中写入administrator)。

添加用户后,即可看到添加的root用户,但是新增用户没有任何权限,需要为用户添加权限。单击root用户名,系统跳转到root用户的管理界面:

该界面包括:

  • Permissions:为root用户设置针对选定的虚拟主机的权限。
  • Topic permissions:为root用户设置针对选定的虚拟主机、选定的Exchange的权限。
  • Update this user:用于更新root用户的密码、Tags信息。
  • Delete this user:用于删除root用户。

Permissions区域用于为root用户设置针对选定的虚拟主机的权限,因此可以通过Virtual Host下拉列表来选择虚拟主机,后面三个文本框用于指定root用户对该虚拟机下的哪些实体具有可配置、可写入、可读取的权限。

Topic permissions:包括用于选择虚拟主机、Exchange,表示为指定的虚拟主机、指定的Exchange设置权限,是更细粒度的权限设置。如果Permission已设置所有实体(包括Exchange)的权限,这里就无须设置了。

点击右边的“Virtual Hosts”,系统进入虚拟主机管理界面。

输入虚拟主机的Name、Tags信息,然后单击“Add virtual host”,即可完成添加虚拟主机。在添加虚拟主机时,只有Name信息是必填信息,Description和Tags都是选填信息。

【虚拟主机的作用】RabbitMQ虚拟主机只是相当于一个命名空间,用于组织Exchange和Queue,比如RabbitMQ可以先在“/”虚拟主机下创建一个名为my.host的Exchange,然后又可以在“test”虚拟主机下创建一个名为my.host的Exchange。

1.4,内部结构,工作机制(★)

RabbitMQ的核心概念:

  • Connection:代表客户端(包括消息生产者和消息消费者)与RabbitMQ之间的连接。
  • Channel:Channel位于连接内部,负责实际的通信。
  • Exchange:充当消息交换的组件。
  • Queue:消息队列。

不管是消息生产者,还是消息消费者,它们都要通过Connection建立与RabbitMQ之间的连接,因此Connection就代表客户端与RabbitMQ之间的连接。客户端与RabbitMQ之间实际通信使用的是Channel(信道),这是因为RabbitMQ采用了类似于Java NIO的做法,避免为应用程序中的每个线程都建立单独的连接。

应用程序的每个线程都能持有自己对象的Channel,因此Channel复用了连接,同时RabbitMQ可以确保每个线程的私密性,就像各自拥有独立的连接一样。当每个Channel的数据流量不是很大时,复用单一的连接可以有效地节省连接资源;当Channel本身的数据流量很大时,多个Channel复用一个连接就会产生性能瓶颈,连接本身的流量限制了所有复用它的Channel的总流量,此时可考虑建立多个连接,并将这些Channel均摊到这些连接中

消息生产者发送消息时,只需指定两个关键信息:

  • Exchange:将该消息发送到哪个Exchange。
  • Routing key:消息的路由key。

与JMS消息模型不同,RabbitMQ的消息生产者不需要指定将消息发送到哪个消息队列,只需要指定将消息发送到哪个Exchange,Exchange相当于消息交换机,它会根据消息的路由key(Routing key)将消息分发到一个或多个消息队列(Queue),消息实际依然由消息队列来负责管理。

消息消费者接收消息时,只需要从指定消息队列中获取消息即可,与Exchange是无关的。

为了让Exchange能将信息分发给消息队列,消息队列需要将自己绑定到Exchange上,Exchange只会将消息分发给绑定到自己的消息队列,没有绑定的消息队列不会得到Exchange分发的消息。将消息队列绑定到Exchange时,也需要指定一个路由key。Exchange就根据发送消息时指定的路由key、绑定消息队列时指定的路由key来决定将消息分发给哪些消息队列。

Exchange的类型也会影响它对消息的分发,Exchange可以分为:

  • fanout:广播Exchange,这种类型的Exchange会将消息广播到所有与它绑定的消息队列。这种类型的Exchange在分发消息时不看路由key。
  • direct:这种类型的Exchange将消息直接发送到路由key对应的消息队列。
  • topic:这种类型的Exchange在匹配路由key时支持通配符。
  • headers:这种类型的Exchange要根据消息自带的头信息进行路由。这种类型的Exchange比较少用。

例如,存在以下绑定: 

  • 当Exchange为fanout类型时,消息生产者向Exchange发送的任何消息都会被分发给3个队列:Q1、Q2、Q3。fanout类型的Exchange在分发消息时不考虑路由key.
  • 当Exchange为direct类型时,消息生产者向Exchange发送路由key为test的消息,该消息只会被分发给Q1和Q2两个队列,因为绑定这两个队列的路由key也是test。
  • 当Exchange为topic时,路由key可以使用通配符,其中“ * ”用于精确匹配一个单词;“#”用于匹配零个或多个单词,单词之间用英文点号隔开。

1.5,Exchange管理

打开“Exchanges”标签页,可以看到其中列出了RabbitMQ内置的Exchange。RabbitMQ会自动为每个虚拟主机创建7个Exchange:其中direct类型的Exchange两个,fanout类型的Exchange一个,headers类型的Exchange两个,topic类型的Exchange两个。

如果需要额外的Exchange,RabbitMQ也允许创建更多的Exchange,通过Web图形用户界面来创建(大部分时候直接使用Java程序来创建):

  • Virtual Host:选择哪个虚拟主机(命名空间)下创建Exchange,如果只有一个则不显示。
  • Name:指定Exchange的名称。
  • Type:指定Exchange的类型,支持fanout、direct、headers、topic类型。
  • Durability:指定该Exchange是否需要持久化保存。
  • Auto delete:指定该Exchange是否会被自动删除。如果启用,则意味着只要该Exchange不再使用(没有消息生产者向它发送消息、没有消息队列与它绑定),它就会被自动删除。
  • Internal:指定是否创建内部Exchange。如果指定为true,则客户端将不能直接向该Exchange发送消息,它只能用于与其他Exchange绑定,接收其他Exchange分发过来的消息。
  • Arguments:指定额外的创建参数。

Durability-持久化】持久化的Exchange能与持久化的队列结合使用,用于确保消息的持久化。如果不使用持久化的消息,当RabbitMQ遇到服务器宕机等故障时,那么未处理的消息可能会丢失;而使用持久化则可以确保消息不会丢失。

使用持久化的消息需要3个条件:

  • 使用持久化的Exchange。
  • 使用持久化的队列。
  • 在发送消息时设置使用持久化的分发模式(将deliveryMode设为2)。

点击“(AMQP default)”的Exchange,可以看到默认的Exchange的类型是direct(Type为direct),并且是持久化保存的Exchange(durable为true)。

从“Bindings”区域看到“The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.”,默认Exchange会被隐式绑定到每个队列(以队列名作为绑定的路由key),不能执行显示绑定或解绑,默认Exchange也不能被删除。这意味着:如果程序向默认Exchange发送路由key为abc的消息,该消息将被分发到名为abc的队列;如果程序向默认Exchange发送路由key为xyz的消息,该消息将被分发到名为xyz的队列——正是JMS的P2P消息模型。

【发送消息】点击“Publish message”,可以看到如下发送消息的界面:

  • 路由key
  • 消息头:可以是任意的。
  • 消息属性:content_type、content_encoding等有特殊意义的名次。
  • 消息体。

填写完成后,点击“Publish message”按钮即可向该Exchange发送消息,该消息将会由该Exchange分发给指定队列。

【绑定队列】点击任意一个非默认Exchange,可以看到如下界面:

在“Bindings”区域可以看到该Exchange当前未绑定任何队列;此外,还可以在该区域为该Exchange执行绑定,填写队列名、路由key,单击Bind按钮,即可完成该Exchange与队列的绑定。

Exchange除了能与队列绑定,还可与其他Exchange绑定,比如内部Exchange不能接收客户端发送的消息,它只能接收由其他Exchange分发过来的消息,因此内部Exchange必须与其他Exchange绑定才会收到消息。

2,RabbitMQ实践 

2.1,利用默认Exchange实现P2P消息模型

【消息消费者】

首先添加Maven依赖

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

该依赖代表了RabbitMQ Java Client,使用该依赖库开发消费者的步骤:

  • 创建ConnectionFactory,设置连接信息,再通过ConnectionFactory获取Connection。
  • 通过Connection获取Channel。
  • 根据需要调用Channel的queueDeclare()方法声明消息队列,如果声明的队列已经存在,该方法将会直接获取已有的队列;如果声明的队列还不存在,该方法将会创建新的队列。
  • 调用Channel的baseConsume()方法开始处理消息,在调用方法时需要传入一个Consumer参数,该参数相当于JMS中的消息监听器。

获取Connection的工具类

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConnectionUtil {
    public static Connection getConnection() throws IOException, TimeoutException{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("root");
        factory.setPassword("123456");
        //如果不设置虚拟主机,则使用默认虚拟主机
        //factory.setVirtualHost("test");
        return factory.newConnection();
    }
}

消息消费者

import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class P2PConsumer {
    final static String QUEUE_NAME="firstQueue";
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建与RabbitMQ服务器连接的TCP连接
        Connection connection = ConnectionUtil.getConnection();
        //创建Channel
        Channel channel = connection.createChannel();
        //声明消息队列,如果队列不存在,则会自动创建该队列
        //true:是否持久化
        //false:是否独享
        //true:是否自动删除
        channel.queueDeclare(QUEUE_NAME,true,false,true,null);
        //创建消息消费者
        Consumer consumer = new DefaultConsumer(channel){
            //每当读到消息队列中的消息时,该方法被自动触发
            //Envelope参数代表消息封包,可获得Exchange名称和路由key
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, StandardCharsets.UTF_8);
                System.out.println(envelope.getExchange()+","+envelope.getRoutingKey()+","+message);
            }
        };
        //从指定消息队列中获取消息
        //true:自动确认
        channel.basicConsume(QUEUE_NAME,true,consumer);
    }
}

消息消费者程序首先声明了一个名为“firstQueue”的消息队列,然后调用Channel的basicConsume()方法从该消息队列中读取信息。消息消费者不需要知道Exchange的存在,它与Exchange是完全解耦的。如果确实要获取Exchange信息,消息消费者可以通过消息监听方法的Envelope参数来获取。

System.out.println(envelope.getExchange()+","+envelope.getRoutingKey()+","+message);

运行上面程序,由于该程序的最后并未关闭Channel和Connection,因此它将一直与RabbitMQ保持连接,除非强制退出程序。打开RabbitMQ的Web控制台,再打开“Queue”标签页:

在默认虚拟主机(/)下有一个名为“firstQueue”的消息队列,这就是上面程序所创建的队列。由于在创建该消息队列时指定了autoDelete为true,这意味着只要该队列不再使用,该队列就会被自动删除,因此如果强制终止程序,该消息队列就会被自动删除。

打开“Connections”标签页,可以看到:

此时有一个客户端与RabbitMQ的虚拟主机(/)保持连接,连接时使用的用户名是root,有1个Channel,还能看到数据传输率。

【消息生产者】

使用RabbitMQ Java Client依赖库开发消息生产者程序的大致步骤:

  • 创建ConnectionFactory,设置连接信息,再通过ConnectionFactory获取Connection。
  • 通过Connection获取Channel。
  • 根据需要调用exchangeDeclare()、queueDeclare()方法声明Exchange和消息队列,并完成队列与Exchange绑定。类似地,如果声明的Exchange还不存在,则创建该Exchange;否则直接使用已有的Exchange。
  • 调用Channel的basicConsume()方法开始处理消息,在调用该方法时需要传入一个Consumer参数,该参数相当于JMS中的消息监听器。

【问题】消息生产者要声明Exchange是自然而然的事情,毕竟消息生产者要向Exchange发送消息,但消息生产者为何还要声明消息队列呢?消息生产者与消息队列不是完全隔离的吗?

【答案】的确是这样的,消息生产者与消息队列是完全隔离的,消息生产者发送消息时也不需要关心消息队列,甚至可以不用理会消息队列是否存在。但是,别忘了RabbitMQ中的消息队列大多是自动删除的队列,这意味着:加入消息生产者程序先运行,而消息消费者还没开始监听,那么系统中就可能暂时还没有任何消息队列。在这种情况下,消息生产者向Exchange发送的消息将不会分发给任何队列,这些消息直接就被丢弃了。

因此,虽然消息生产者与消息队列是隔离的,消息生产者发送消息时与消息队列无关,但实际上它依然需要确保该Exchange所分发消息的队列是存在的,且这些队列已和该Exchange执行了绑定,否则有可能该Exchange没绑定任何队列,那么发送给该Exchange的所有消息都将直接被丢弃。

所以,消息生产者程序通常都需要声明Exchange和消息队列,并执行Exchange与消息队列的绑定,用于确保该Exchange所分发消息的队列是存在的,且与该Exchange执行了绑定。

由于该消息生产者程序打算使用RabbitMQ的默认Exchange(该Exchange不能被删除),因此无须声明Exchange,且所有队列都会自动绑定到默认Exchange,所以也不需要显示执行绑定。

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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class P2PProducer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //使用自动关闭资源的try语句管理Connection、Channel
        //创建与RabbitMQ服务的TCP连接
        Connection connection = ConnectionUtil.getConnection();
        //创建Channel
        Channel channel = connection.createChannel();
        //声明消息队列
        channel.queueDeclare(P2PConsumer.QUEUE_NAME, true, false, true, null);
        for (int i = 1; i < 11; i++) {
            String msg = "第" + i + "条消息";
            channel.basicPublish(EXCHANGE_NAME, P2PConsumer.QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("已发送的消息:" + msg);
        }
    }
}

2.2,工作队列(Work Queue)

RabbitMQ可以让多个消息消费者竞争消费同一个消息消息队列,这种方式被称为工作队列。

当多个消息消费者竞争同一个消息队列时,消息队列默认会将消息“均分”给每个消息消费者,但这样做往往并不合适,因为有的消息消费者需要更多的时间来处理一条消息,而有的消息消费者只需要更少的时间即可处理一条消息,如果让它们“均分”这些消息,就会造成资源浪费。

比较理想的做法是“能者多劳”,让消息队列将消息多分给需要更少时间的消息消费者,将消息少分给需要更多时间的时间消费者。但是消息队列根本没办法知道消费者需要的时间!

Channel提供了一个basicQos(int prefetchCount)方法,该方法指定消息消费者在同一时间点能得到的消息数量。假如设置basicQos(1),这意味着每个消息消费者在同一个时间点最多只能得到一条消息。换句话说,在消息队列收到该消息消费者的确认之前,消息队列不会将新的消息分发给该消息消费者,而是将消息分给其他处于空闲状态(已经返回确认)的消息消费者。

可见basicQos(1)依赖于消息消费者返回的确认信息,如果采用自动确认策略,程序只要进入消息消费者的handleDelivery方法,程序就会立即向消息队列发送确认信息,完全不管handleDelivery()方法是否执行完成,甚至不管该方法是否抛出异常。

为了更精准地控制消息确认,取消消息的自动确认,改为在handleDelivery()方法成功执行完成后手动确认消息。

import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class ConsumerTest {
    final static String QUEUE_NAME = "firstQueue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建与RabbitMQ服务器的TCP连接
        Connection connection = ConnectionUtil.getConnection();
        //创建Channel
        Channel channel = connection.createChannel();
        //声明消息队列
        channel.queueDeclare(QUEUE_NAME, true, false, true, null);
        //设置该Channel在同一时间点只能得到一条消息
        channel.basicQos(1);
        //创建消息消费者
        Consumer consumer = new DefaultConsumer(channel) {
            //每当读到消息队列中的消息时,该方法会被自动触发
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, StandardCharsets.UTF_8);
                //耗时操作
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(envelope.getExchange() + "," + envelope.getRoutingKey() + "," + message);
                //确定消息处理完成
                //false:是否同时确认该消息之前所有未确定的消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //false:不自动确认
        channel.basicConsume(QUEUE_NAME,false,consumer);
    }
}

同样设置另外一个消息消费者,只不过没有耗时操作,运行可以得到如下结果:

如果设置了自动确认,只不过左边的是2s打印一次,右边很快打印:

2.3,使用fanout实现Pub-Sub消息模型

【消息生产者】fanout类型的Exchange不会判断消息的路由key,该Exchange直接将消息分发给绑定到它的所有队列。

消息生产者发送一条消息到fanout类型的Exchange后,绑定到该Exchange的所有队列都会收到该消息的一个副本,而消息消费者分别从不同的队列中读取消息,互不干扰。fanout类型的Exchange可以很好地模拟JMS中的Pub-Sub消息模型。

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducer {
    public final static String EXCHANGE_NAME = "test";
    public final static String ROUING_KEY = "myKey";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);
        channel.queueDeclare("FirstConsumer", true, false, true, null);
        channel.queueDeclare("SecondConsumer", true, false, true, null);
        channel.queueBind("FirstConsumer", EXCHANGE_NAME, ROUING_KEY, null);
        channel.queueBind("SecondConsumer", EXCHANGE_NAME, ROUING_KEY, null);
        for (int i = 0; i < 11; i++) {
            String msg = "第" + i + "条消息";
            channel.basicPublish(EXCHANGE_NAME, P2PConsumer.QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("已发送的消息:" + msg);
        }
    }
}

上面代码声明了一个类型为fanout的Exchange,确保该Exchange的存在,但并不保证一定会创建新的Exchange。因此代码存在一个风险:如果当前虚拟主机已有同名的Exchange,且它的类型不是fanout,那么就会抛出异常。所以在大部分应用中,总是创建自动删除的Exchange是一种不错的做法:用Exchange时就声明,声明语句总能确保该Exchange的存在,用完Exchange就自动删除,避免后续引发异常。

【消息消费者】先声明消息队列来确保队列存在,然后调用basicConsume()方法从指定队列中读取消息即可。

import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
    final static String QUEUE_NAME = "FirstConsumer";
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建与RabbitMQ服务器的TCP连接
        Connection connection = ConnectionUtil.getConnection();
        //创建Channel
        Channel channel = connection.createChannel();
        //声明消息队列
        channel.queueDeclare(QUEUE_NAME, true, false, true, null);
        //创建消息消费者
        Consumer consumer = new DefaultConsumer(channel) {
            //每当读到消息队列中的消息时,该方法会被自动触发
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, StandardCharsets.UTF_8);
                System.out.println(envelope.getExchange() + "," + envelope.getRoutingKey() + "," + message);
            }
        };
        channel.basicConsume(QUEUE_NAME,true,consumer);
    }
}

2.4,使用direct实现消息路由

direct类型的Exchange会根据消息的路由key将消息分发给指定的队列,一个队列能与一个Exchange绑定多个路由key:

RabbitMQ也允许多个队列绑定相同的路由key,此时又变成了Pub-Sub消息模型:

可以组合上面两种方式,如日志系统:

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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class LogProducer {
    public final static String EXCHANGE_NAME = "test";
    final static String[] ROUING_KEYS = {"info", "warning", "error"};

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, true, null);
        channel.queueDeclare("FirstConsumer", true, false, true, null);
        channel.queueDeclare("SecondConsumer", true, false, true, null);
        channel.queueBind("FirstConsumer", EXCHANGE_NAME, ROUING_KEYS[2], null);
        for (int i = 0; i < ROUING_KEYS.length; i++) {
            channel.queueBind("SecondConsumer", EXCHANGE_NAME, ROUING_KEYS[i], null);
        }
        for (int i = 0; i < 31; i++) {
            //根据i动态决定路由key
            String routingKey = i<11?ROUING_KEYS[0] :(i<21?ROUING_KEYS[1] :ROUING_KEYS[2] );
            String msg = "第" + i + "条消息";
            channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("已发送的消息:" + msg);
        }
    }
}

先运行LogProducer,再运行Consumer1和Consumer2,因为使用的是非默认Exchange。

2.5,使用topic实现通配符路由

topic类型的Exchange支持在路由key中使用通配符,路由key一般由一个或多个单词组成,多个单词之间以“.”分隔。

  • *:匹配一个单词。
  • #:匹配零个或多个单词。

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class TopicProducer {
    public final static String EXCHANGE_NAME="test";
    public final static String[] ROUING_KEYS={"www.baidu.com","www.baidu.cn","edu.baidu.org","edu.google.org","google.org"};
    public final static String[] KEY_PATTERNS={"*.baidu.*","*.org","edu.#"};

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true, false, null);
        channel.queueDeclare("FirstConsumer", true, false, true, null);
        channel.queueDeclare("SecondConsumer", true, false, true, null);
        channel.queueBind("FirstConsumer", EXCHANGE_NAME, KEY_PATTERNS[0], null);
        for (int i = 1; i < KEY_PATTERNS.length; i++) {
            channel.queueBind("SecondConsumer", EXCHANGE_NAME, ROUING_KEYS[i], null);
        }
        for (int i = 0; i < ROUING_KEYS.length; i++) {
            String msg = "第" + (i+1) + "条消息";
            channel.basicPublish(EXCHANGE_NAME, ROUING_KEYS[i], null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("已发送的消息:" + msg);
        }
    }
}

3,Spring整合RabbitMQ

3.1,RPC通信模型

通过使用两个独享队列,可以让RabbitMQ实现RPC(远程过程调用)通信模型,其通信过程其实很简单:客户端向服务器消费独享的队列发送一条消息,服务器收到该消息后,对该消息进行处理,然后将处理结果发送给客户端消费的独享队列。使用独享队列可以避免其他连接读取该队列中的消息,只有单前连接才能读取该队列中的消息,这样才可以保证服务器能读到客户端发送的每条消息,客户端也能读到服务器返回的每条消息。

为了让服务器知道客户端所消费的独享队列,客户端发送消息时,应该将自己监听的队列名以reply_to参数发送给服务器;为了能精准识别服务器应答消息与客户端请求消息之间的对应关系,还需要为每条消息都增加一个correlation_id属性,两条具有相同的correlation_id属性值的消息,可认为它们是配对的两条消息。

  • 服务器启动时,它会创建一个名为“rpc_queue”的独享队列,并使用服务器的消费者监听该独享队列的消息。
  • 客户端启动时,它会创建一个匿名的独享队列(由RabbitMQ命名),并使用客户端的消费者监听该独享队列的消息。
  • 客户端发送带有两个属性的消息:一个代表应答队列名为reply_to属性;另一个代表消息标识的correlation_id属性。
  • 将消息发送到服务器监听的rpc_queue队列中。
  • 服务器从rpc_queue队列中读取消息,服务器调用处理程序对消息进行计算,将计算结果以消息的形式发送给reply_to属性指定的队列,并为消息添加correlation_id属性。
  • 客户端从reply_to对应的队列中读取消息,当消息出现时,它会检查消息的correlation_id属性。如果此属性的值与请求消息的correlation_id属性的值匹配,则将它返回给应用。
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

public class Server {
    public static final String SERVER_QUEUE = "rpc_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(SERVER_QUEUE, true, true, true, null);
        Consumer consumer = new DefaultConsumer(channel) {
            //每当读到消息队列中的消息时,该方法被自动触发
            //Envelope参数代表消息封包,可获得Exchange名称和路由key
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                int number = Integer.parseInt(new String(body, StandardCharsets.UTF_8));
                String replyQueue = properties.getReplyTo();
                String correlationId = properties.getCorrelationId();
                channel.basicPublish("", replyQueue, new AMQP.BasicProperties.Builder()
                        .correlationId(correlationId + "")
                        .deliveryMode(2)    //持久化,防止消息丢失
                        .build(), (cal(number) + "").getBytes(StandardCharsets.UTF_8));
            }
        };
        channel.basicConsume(SERVER_QUEUE, true, consumer);
    }
    public static int cal(int n) {
        int result = 1;
        for (int i = 2; i <= n; i++) {
            result *= i;
        }
        return result;
    }
}
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Client {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String replayQueue = channel.queueDeclare().getQueue();
        Consumer consumer = new DefaultConsumer(channel) {
            //每当读到消息队列中的消息时,该方法会被自动触发
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, StandardCharsets.UTF_8);
                String correlationId = properties.getCorrelationId();
                System.out.println(correlationId + "," + "返回的消息为:" + message);
            }
        };
        //等待从指定队列中获取消息
        channel.basicConsume(replayQueue, true, consumer);
        for (int i = 1; i <= 10; i++) {
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().replyTo(replayQueue).correlationId(i + "").build();
            channel.basicPublish("", Server.SERVER_QUEUE, properties, (i + "").getBytes(StandardCharsets.UTF_8));
        }
    }
}

通过消息机制实现的RPC具有很多优势,比如Client虽然调用了Server的cal()方法,但Client其实不知道Server的存在,它与Server是完全解耦的,因此Server即可以是应用内部的一个程序,也可以是来自另外一个分布式应用的程序,甚至可以是其他语言所实现的程序——这一切都没关系,总之,只要服务器程序也面向消息编程即可。

3.2,SpringBoot的RabbitMQ支持

SpringBoot提供了一个spring-boot-starter-amqp的Starter来支持RabbitMQ,只要添加该Starter,它就会添加spring-rabbit依赖库。SpringBoot基于标准的AMQP与RabbitMQ通信。

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

只要SpringBoot检测到类加载路径下包含了spring-rabbit依赖库,它就会自动配置CachingConnectionFactory,还会自动配置AmqpAdmin和AmqpTemplate,其中AmqpAdmin提供了如下常用方法:

  • void declareExchange(Exchange exchange):声明Exchange。
  • String declareQueue(Queue queue):声明队列。
  • Queue declareQueue():声明服务器命名的、独享的、会自动删除的、非持久化的队列。
  • declareBinding(Binding binding):声明队列或Exchange与Exchange的绑定。
  • boolean deleteExchange(String exchangeName):删除Exchange。
  • boolean deleteQueue(String queueName):无条件地删除队列。
  • void deleteQueue(String queueName, boolean unused, boolean empty):删除队列,只有当该队列不再使用且没有消息时才删除。
  • void removeBinding(Binding binding):解除绑定。

AmqpAdmin的作用就是管理Exchange、队列和绑定。而AmqpTemplate则用于发送、接收消息,包括:

  • convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor):自动将message参数转换成消息发送给exchange。在发送之前,还可通过messagePostProcessor参数对消息进行修改。routingKey为要发送的路由地址。
  • convertSendAndReceive(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor):自动将message参数转换成消息发送给exchange。在发送之前,还可通过messagePostProcessor参数对消息进行修改。
  • send(String exchange, String routingKey, Message message):发送消息。
  • sendAndReceive(String exchange, String routingKey, Message message):该方法在发送请求之后会等待返回的消息。
  • receive(String queueName, long timeoutMillis):指定从queueName队列中接收消息。

AmqpAdmin和AmqpTemplate加起来就是RabbitMQ Client中的Channel的功能。

#设置RabbitMQ的主机和端口
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
#或者下面代替
#spring.rabbitmq.addresses=amqp://root:123456@localhost:5672

#设置缓存Channel还是Connection
spring.rabbitmq.cache.connection.mode=channel
#设置缓存Channel的数量
spring.rabbitmq.cache.channel.size=20

#启用AmqpTemplate的自动重试功能
spring.rabbitmq.template.retry.enabled=true
#设置自动重试的时间间隔为2秒
spring.rabbitmq.template.retry.initial-interval=2s
#设置AmqpTemplate的默认Exchange为“”
spring.rabbitmq.template.exchange=""
#设置AmqpTemplate的默认路由key为“test”
spring.rabbitmq.template.routing-key=test

3.3,消息生产者

SpringBoot可以将AmqpAdmin和AmqpTemplate注入任何其他组件,接下来该组件即可通过AmqpAdmin来管理Exchange、队列和绑定,还可通过AmqpTemplate来发送消息。

@Service
public class MessageService {
    public static final String EXCHANGE_NAME = "boot.fanout";
    public static final String[] QUEUE_NAME = {"myQueue1", "myQueue2"};
    private final AmqpAdmin amqpAdmin;
    private final AmqpTemplate amqpTemplate;

    public MessageService(AmqpAdmin amqpAdmin,AmqpTemplate amqpTemplate) {
        this.amqpAdmin = amqpAdmin;
        this.amqpTemplate = amqpTemplate;
        //创建Exchange对象,根据Exchange类型的不同
        //可使用DirectExchange、FanoutExchange、HeadersExchange、FanoutExchange
        FanoutExchange exchange = new FanoutExchange(EXCHANGE_NAME, true, true);
        //声明Exchange
        this.amqpAdmin.declareExchange(exchange);
        //使用循环声明并绑定两个队列
        for (String queueName : QUEUE_NAME) {
            Queue queue = new Queue(queueName, true, false, true);
            //声明队列
            this.amqpAdmin.declareQueue(queue);
            Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, EXCHANGE_NAME, "", null);
            this.amqpAdmin.declareBinding(binding);
        }
    }

    public void produce(String message) {
        this.amqpTemplate.convertAndSend(EXCHANGE_NAME, "", message);
    }
}
@RestController
public class HelloController {
    private final MessageService messageService;
    public HelloController(MessageService messageService){
        this.messageService = messageService;
    }
    @GetMapping("/produce/{message}")
    public String produce(@PathVariable String message){
        messageService.produce(message);
        return "发送消息";
    }
}

3.4,消息消费者

SpringBoot会自动将@RabbitListener注解修饰的方法注册为消息监听器。如果没有显示配置监听容器工厂(RabbitListenerContainerFactory),SpringBoot会在容器中自动配置一个SimpleRabbitListenerContainerFactory Bean作为监听容器工厂。如果希望使用DirectRabbitListenerContainerFactory,则可在application.properties文件中添加如下配置:

spring.rabbitmq.listener.type=direct

Consumer消费者通过@RabbitListener注解创建侦听器端点,绑定rabbitmq_queue队列。

  • @RabbitListener注解提供了@QueueBinding、@Queue、@Exchange等对象,通过这个组合注解配置交换机、绑定路由并且配置监听功能等。
  • @RabbitHandler注解为具体接收的方法。
@Component
public class Consumer {
    @RabbitListener(queues = "myQueue1")
    public void processMessage(String content) {
        System.out.println("从myQueue1收到消息:" + content);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕双嘤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值