文章目录
RabbitMQ是什么
RabbitMQ是一个开源的、可靠的消息代理。它应用非常广泛,不管是小型企业还是大型公司,RabbitMQ都能胜任
RabbitMQ相关的一些基本概念
- 生产者
生产者是发送消息给RabbitMQ的应用程序,它不属于RabbitMQ - 消费者
消费者是监听并消费RabbitMQ消息的应用程序,它不属于RabbitMQ - 虚拟主机
一个RabbitMQ可以创建多个虚拟主机,一个虚拟主机可以创建多个交换机和队列。虚拟主机起到权限隔离、数据隔离的作用 - 交换机
交换机负责接收生产者发送的消息,并将消息根据路由key转发到绑定的队列中,常见的交换机类型有fanout(广播)、direct(点对点)、topic(主题) - 队列
队列是一个消息缓冲区,负责接收并存储消息 - 绑定关系
队列会与交换机进行绑定 - 路由key
路由key决定了交换机与队列的绑定关系,也决定了交换机最终会把消息转发到哪个队列中 - 消息
消息是一个二进制数据,最终会存储到队列中
消息的生产消费的流程
生产者生产并发送消息,交换机接收到消息后转发到指定的队列中,然后消费者监听队列,如果队列中有消息,则消费者会消费消息。我们可以将这些角色与邮局进行类比,生产者就相当于发件人,消息就相当于邮件,交换机就相当于发件箱与邮递员,队列就相当于收件箱,消费者就相当于收件人,这样理解就清晰多了。
RabbitMQ的下载、安装、运行
Docker一键运行RabbitMQ容器
docker run -d -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.13-management
其中 5672 是客户端与RabbitMQ的通信端口,15672 是RabbitMQ控制台访问端口
最后打印出了容器Id,则代表RabbitMQ已启动成功了
RabbitMQ控制台使用
- 登录RabbitMQ控制台
- 控制台访问链接:http://{your ip}:15672
- 账号/密码:guest/guest
进入控制台后可以看到以下界面,我这里使用的RabbitMQ版本是3.13.1,Erlang是26.2.4
-
创建一个direct类型的交换机
-
创建一个队列
-
交换机绑定队列
-
发送一条消息
-
消费一条消息
如上图,我们在控制台简单演示了如何创建交换机、队列,如何绑定交换机和队列以及如何发送和消费消息,接下来我们用Java代码来实现类似的效果
使用Java客户端生产与消费消息
最简模式
最简模式的消息结构如下图,一个生产者负责发送消息给队列,然后一个消费者负责监听队列并消费消息。其实这里没有画出交换机,因为这里使用的是RabbitMQ的默认交换机,默认交换机是direct类型。一个队列被创建出来后就会自动绑定默认交换机,绑定的路由key是队列名
- 创建一个连接工厂工具类
/**
* 获取连接
* @return
*/
public static Connection getConnection() {
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); // RabbitMQ主机IP
factory.setPort(5672); // RabbitMQ通信端口
factory.setVirtualHost("/"); // 设置虚拟主机,每个虚拟主机就相当于一个消息代理
factory.setUsername("guest");
factory.setPassword("guest");
return factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return null;
}
- 编写生产者程序
public static void main(String[] args) throws IOException, TimeoutException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、生产消息
// 3.1 声明队列
String queueName = "basic_q";
channel.queueDeclare(queueName, true, false, false, null);
// 3.2 发送消息
String msg = "hello basic";
channel.basicPublish("", queueName, null, msg.getBytes());
System.out.println("发送了一条消息:" + msg);
// 4、关闭连接
channel.close();
connection.close();
}
}
- 编写消费者程序
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明队列
String queueName = "basic_q";
channel.queueDeclare(queueName, true, false, false, null);
// 3.2 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.3 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body));
}
};
channel.basicConsume(queueName, true, consumer);
}
}
-
启动消费者
-
启动生产者
-
测试结果
-
说明
- 生产者与消费者程序声明的队列名要一致
- 启动生产者或消费者程序时,如果声明的队列不存在,则会创建队列,否则不会重复创建
- 消费者程序启动后会阻塞并监听队列,如果队列有消息则会进行消费
工作模式
工作模式的消息结构如下图,一个生产者负责发送消息给队列,然后多个消费者负责监听队列并消费消息。这里使用的交换机依然是默认交换机。一个队列中的消息只能被消费一次,默认情况下,监听队列的多个消费者会轮询消费消息
- 编写生产者程序
public static void main(String[] args) throws IOException, TimeoutException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、生产消息
// 3.1 声明队列
String queueName = "work_q";
// durable 队列的持久化,防止消息代理挂了导致队列和消息丢失
channel.queueDeclare(queueName, true, false, false, null);
// 3.2 发送多条消息
for (int i = 0; i < 12; i++) {
String msg = "hello work" + i;
channel.basicPublish("", queueName, null, msg.getBytes());
System.out.println("发送了一条消息:" + msg);
}
// 4、关闭连接
channel.close();
connection.close();
}
}
- 编写消费者程序
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明队列
String queueName = "work_q";
channel.queueDeclare(queueName, true, false, false, null);
// 3.2 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.3 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body));
}
};
channel.basicConsume(queueName, true, consumer);
}
}
- 启动消费者
- 可以自行复制一份消费者代码,然后启动两个消费者去监听队列
- 启动生产者
- 测试结果
- 说明
可以看到两个消费者是轮询消费消息的
广播模式
广播模式的消息结构如下图,一个生产者发送消息给交换机,交换机将消息转发到所有与它绑定的队列中。广播模式的交换机类型是fanout
- 编写生产者程序
public static void main(String[] args) throws IOException, TimeoutException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、生产消息
// 3.1 声明交换机
String exchangeName = "fanout_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 3.2 发送消息
String msg = "hello fanout";
channel.basicPublish(exchangeName, "", null, msg.getBytes());
System.out.println("发送了一条消息:" + msg);
// 4、关闭连接
channel.close();
connection.close();
}
}
- 编写消费者程序
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "fanout_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 3.2 声明队列
String queueName = "fanout_q1";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body) + ";将要发送短信");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "fanout_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 3.2 声明队列
String queueName = "fanout_q2";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body) + ";将要发送邮件");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
-
启动消费者
-
启动生产者
-
测试结果
-
说明
- 启动生产者或消费者程序时,如果声明的交换机不存在,则会创建交换机,否则不会重复创建
- 生产者与消费者声明的交换机名和交换机类型要一致
点对点模式
点对点模式的消息结构如下图,一个生产者发送消息给交换机,然后交换机根据消息携带的路由key转发到精确匹配的队列中。点对点模式的交换机类型是direct
- 编写生产者程序
public static void main(String[] args) throws IOException, TimeoutException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、生产消息
// 3.1 声明交换机
String exchangeName = "direct_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
// 3.2 发送消息
String msg = "hello direct";
channel.basicPublish(exchangeName, "email", null, msg.getBytes());
System.out.println("发送了一条消息:" + msg);
// 4、关闭连接
channel.close();
connection.close();
}
}
- 编写消费者程序
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "direct_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
// 3.2 声明队列
String queueName = "direct_q1";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "sms");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body)+";将要发送短信");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "direct_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
// 3.2 声明队列
String queueName = "direct_q2";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "email");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body) + ";将要发送邮件");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
-
启动消费者
-
启动生产者
-
测试结果
-
说明
- 只有路由key是email的消费者消费了消息
主题模式
主题模式的消息结构如下图,一个生产者发送消息给交换机,然后交换机根据消息携带的路由key转发到匹配的队列中。主题模式的路由key是可以有通配符的,其中#代表一个任意单词,*代表0个或多个任意单词,单词之间用.隔开。主题模式的交换机类型是topic。主题模式与点对点模式最大的不同是主题模式的路由key是有通配符的,可以模糊匹配队列
- 编写生产者程序
public static void main(String[] args) throws IOException, TimeoutException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、生产消息
// 3.1 声明交换机
String exchangeName = "topic_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
// 3.2 发送消息
String msg = "hello topic";
channel.basicPublish(exchangeName, "sms.we", null, msg.getBytes());
System.out.println("发送了一条消息:" + msg);
// 4、关闭连接
channel.close();
connection.close();
}
}
- 编写消费者程序
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "topic_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
// 3.2 声明队列
String queueName = "topic_q1";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "email.*");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body)+";将要发送邮件");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
public static void main(String[] args) throws IOException {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
if (Objects.nonNull(connection)) {
// 2、建立通道
Channel channel = connection.createChannel();
// 3、消费消息
// 3.1 声明交换机
String exchangeName = "topic_e";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
// 3.2 声明队列
String queueName = "topic_q2";
channel.queueDeclare(queueName, true, false, false, null);
// 3.3 绑定交换机
channel.queueBind(queueName, exchangeName, "sms.#");
// 3.4 监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 3.5 消费消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我消费了消息:" + new String(body)+";将要发送短信");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
-
启动消费者
-
启动生产者
-
测试结果
-
说明
- 只有路由key是sms.#的消费者消费了消息
Spring Boot整合RabbitMQ
本次整合示例使用topic类型的交换机
- 创建一个Spring Boot项目
我使用的Spring Boot版本是2.3.6.RELEASE,您可以自行选择版本
- 配置maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
- 配置RabbitMQ连接信息
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: guest
password: guest
- 编写生产者程序
@RestController
@RequestMapping("/topic")
public class TopicController {
@Autowired
private RabbitTemplate rabbitTemplate;
public static final String TOPIC_EX_NAME = "sb_topic_e";
/**
* @Description: 只发送短信
* @Date: 2023/6/8
*/
@GetMapping("/sendSms")
public String sendSms() {
rabbitTemplate.convertAndSend(TOPIC_EX_NAME, "sms", "发送短信");
return "success";
}
/**
* @Description: 发送短信和邮件
* @Date: 2023/6/8
*/
@GetMapping("/sendSmsAndEmail")
public String sendSmsAndEmail() {
rabbitTemplate.convertAndSend(TOPIC_EX_NAME, "sms.email", "发送短信和邮件");
return "success";
}
}
- 编写消费者程序
@Component
public class TopicConsume {
public static final String TOPIC_EX_NAME = "sb_topic_e";
public static final String QUEUE_EX_NAME_ONE = "sb_topic_que_one";
public static final String QUEUE_EX_NAME_TWO = "sb_topic_que_two";
public static final String TOPIC_KEY1 = "sms";
public static final String TOPIC_KEY2 = "sms.*";
/**
* @Description: 监听短信发送
* @Date: 2023/6/8
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue(QUEUE_EX_NAME_ONE), exchange = @Exchange(name = TOPIC_EX_NAME, type = "topic"), key = {TOPIC_KEY1}))
public void receiveSms(String msg) {
System.out.println("短信消费方收到了消息:" + msg);
}
/**
* @Description: 监听短信和邮件发送
* @Date: 2023/6/8
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue(QUEUE_EX_NAME_TWO), exchange = @Exchange(name = TOPIC_EX_NAME, type = "topic"), key = {TOPIC_KEY2}))
public void receiveSmsAndEmail(String msg) {
System.out.println("短信和邮件消费方收到了消息:" + msg);
}
}
- 启动项目访问测试