Producer生产者 -> MQ -> 消费者
RabbitMQ Tutorials:
https://www.rabbitmq.com/getstarted.html
package com.hx.demo.simple;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author Huathy
* @date 2023-03-21 22:01
* @description 简单模式Simple
* 所有中间件技术都是基于TCP/IP协议基础上,构建新型的协议规范,只不过rabbitmq遵循的是amqp
* IP port
*/
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = "queue1";
/**
* 队列名称
* 是否持久化:durable=false 所谓持久化消息是否存盘。那么非持久化的消息是否存盘?会存盘,但重启会丢失。
* 是否具有排他性:是否独占独立。
* 是否自动删除:随着最后一个消费者消费完毕,是否把队列删除
* 额外参数:携带附属参数
*/
// channel.queueDeclare(queueName, false, false, false, null);
// 生成一个持久化的队列
channel.queueDeclare("queue2", true, false, false, null);
// 5. 发送消息给队列queue
String msg = "Hello Huathy";
// 参数: 交换机 、 队列(路由key)、消息的控制状态、消息主题
channel.basicPublish("", queueName, null, msg.getBytes());
System.out.println("消息发送完成......");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
package com.hx.demo.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author Huathy
* @date 2023-03-21 22:52
* @description
*/
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = "queue1";
channel.basicConsume(queueName, true, (consumerTag, delivery) -> {
System.out.println("收到消息:" + new String(delivery.getBody(), "UTF-8"));
}, consumerTag -> {
System.out.println("接收失败了...");
});
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
Advanced Message Queuing Protocol高级消息队列协议。是应用层协议的一个开放标准,为面向消息的中间件设计。
RabbitMQ 为什么是基于通道channel而不是基于连接connection处理?
连接是短链接,经过三四握手四次挥手,消耗时间。开关连接消耗性能。
而通过长连接,创建多个信道,提高性能。
如何保证消费者的可靠消费?利用ASK机制和手动ASK。
消息投递到交换机再放到队列中
核心概念:
Server : 又称Broker ,接受客户端的连接,实现AMQP实体服务。 安装rabbitmq-server
Connection : 连接,应用程序与Broker的网络连接 TCP/IP/三次握手和四次挥手
Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立对各Channel,每个Channel代表一个会话任务。
Message :消息: 服务与应用程序之间传送的数据,由Properties和body组成,Properties可是对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则就是消息体的内容。
Virtual Host: 虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机理由可以有若干个Exhange和Queueu,同一个虚拟主机里面不能有相同名字的Exchange
Exchange: 交换机,接受消息,根据路由键发送消息到绑定的队列。(不具备消息存储的能力)
Bindings : Exchange和Queue之间的虚拟连接,binding中可以保护多个routing key.
Routing key : 是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息
Queue : 队列: 也成为Message Queue,消息队列,保存消息并将它们转发给消费者.
可以存在没有交换机的队列么?不可以。虽然没有指定,但是一定会存在一个默认的交换机。
VirtualPort类比为磁盘目录:隔离区分目的
Name:队列名称
Feature:D表示持久化,Args表示参数
遵循AMQP协议,任何队列都必须绑定交换机。如果没有指定交换机,那么就会绑定默认的交换机。
应用场景:注册发送邮件、短信等可以广播来实现消息同步
Fanout模式指定路由key是没有意义的
public class Producer {
private static String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = "fanout1";
/**
* 队列名称
* 是否持久化:durable=false 所谓持久化消息是否存盘。那么非持久化的消息是否存盘?会存盘,但重启会丢失。
* 注意如果新建的队列是持久化的这里就要是true,否则报错
* 是否具有排他性:是否独占独立。
* 是否自动删除:随着最后一个消费者消费完毕,是否把队列删除
* 额外参数:携带附属参数
*/
channel.queueDeclare(queueName, true, false, false, null);
// 5. 发送消息给队列queue
String msg = "Hello Huathy";
// 参数: 交换机 、 队列(路由key)、消息的控制状态、消息主题
// 这种模式下写不写路由key都无效,但是不可以为null
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
System.out.println("消息发送完成......");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
public class Consumer {
private static Runnable run = new Runnable() {
@SneakyThrows
@Override
public void run() {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = Thread.currentThread().getName();
channel.basicConsume(queueName, true, (consumerTag, delivery) -> {
System.out.println(queueName + "收到消息:" + new String(delivery.getBody(), "UTF-8"));
}, consumerTag -> {
System.out.println("接收失败了...");
});
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
};
public static void main(String[] args) throws IOException, TimeoutException {
new Thread(run, "fanout1").start();
new Thread(run, "fanout2").start();
}
}
在发布订阅模式的基础上增加路由key来做一个选择。 只有对应key的队列才能收到消息。
支持模糊匹配的路由key。对key做模糊匹配。匹配上的key才能收到消息。
#
:0个或多个
*
:有且只能有1个
参数模式可以根据参数来进行过滤。
public class Producer {
final static String EXCHANGE_NAME = "direct-msg-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
/**
* 队列名称
* 是否持久化:durable=false 所谓持久化消息是否存盘。那么非持久化的消息是否存盘?会存盘,但重启会丢失。
* 是否具有排他性:是否独占独立。
* 是否自动删除:随着最后一个消费者消费完毕,是否把队列删除
* 额外参数:携带附属参数
*/
// 定义一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
// 生成一个持久化的队列
String queue = EXCHANGE_NAME.concat("-1");
channel.queueDeclare(queue, true, false, false, null);
channel.queueBind(queue, EXCHANGE_NAME, "order");
// 5. 发送消息给队列queue
String msg = "Hello Huathy";
// 参数: 交换机 、 队列(路由key)、消息的控制状态、消息主题
channel.basicPublish(EXCHANGE_NAME, "order", null, msg.getBytes());
System.out.println("消息发送完成......");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = Producer.EXCHANGE_NAME.concat("-1");
channel.basicConsume(queueName, true, (consumerTag, delivery) -> {
System.out.println("收到消息:" + new String(delivery.getBody(), "UTF-8"));
}, consumerTag -> {
System.out.println("接收失败了...");
});
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
当有多个消费者时,我们的消息会被哪个消费者消费呢?又应该如何均衡消费者消费消息的多少?
主要有俩种消费方式:
该模式接收消息是当有多个消费者接入时,消息的分配模式是一个消费者分配一条,直到消息消费完成。
public class Producer {
private static String EXCHANGE_NAME = "";
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("生产者");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备消息内容
String queueName = "fanout1";
/**
* 队列名称
* 是否持久化:durable=false 所谓持久化消息是否存盘。那么非持久化的消息是否存盘?会存盘,但重启会丢失。
* 注意如果新建的队列是持久化的这里就要是true,否则报错
* 是否具有排他性:是否独占独立。
* 是否自动删除:随着最后一个消费者消费完毕,是否把队列删除
* 额外参数:携带附属参数
*/
channel.queueDeclare(queueName, true, false, false, null);
// 5. 发送消息给队列queue
for (int i = 0; i < 20; i++) {
String msg = i + " Huathy" + SimpleDateFormat.getDateTimeInstance().format(new Date());
// 参数: 交换机 、 队列(路由key)、消息的控制状态、消息主题
channel.basicPublish(EXCHANGE_NAME, "work1", null, msg.getBytes());
System.out.println(i + "消息发送完成......");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
这里节省篇幅只展示1个的示例
public class Work1 {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("work1的消费者1");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备接收消息内容
System.out.println("work1的消费者01 开始接收消息:");
channel.basicConsume("work1", true, (consumerTag, delivery) -> {
System.out.println("work1的消费者01" + "收到消息:" + new String(delivery.getBody(), "UTF-8"));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, consumerTag -> {
System.out.println("接收失败了...");
});
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
需要基于上面的轮询分发模式进行修改:
finalChannel.basicQos(1);
channel.basicConsume("work1", false, (consumerTag, delivery)->{});
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
这里节省篇幅只展示1个的示例
public class FWork1 {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建链接工程
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin123");
factory.setVirtualHost("/");
// 2. 创建链接connection
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("work1的消费者1");
// 3. 创建交换机、队列、绑定关系、路由key、发送消息、接收消息
channel = connection.createChannel();
// 4. 准备接收消息内容
System.out.println("work1的消费者01 开始接收消息:");
Channel finalChannel = channel;
// 公平分发还必须将指标qos定义出来,每次的指标为1获取1条。需要根据当前服务器内存、CPU来设置大小
finalChannel.basicQos(3);
channel.basicConsume("work1", false, (consumerTag, delivery) -> {
System.out.println("work1的消费者01" + "收到消息:" + new String(delivery.getBody(), "UTF-8"));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}, consumerTag -> {
System.out.println("接收失败了...");
});
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭连接
if (channel != null && channel.isOpen()) {
channel.close();
}
// 7. 关闭通道
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
}
存在问题:耦合度高、需要自己写线程池维护成本高、出现了消息可能丢失需要自己来做补偿、需要自己实现保证消息的可靠性、高可用
编写配置
server:
port: 8889
spring:
# 配置rabbitMQ服务
rabbitmq:
port: 5672
username: admin
password: admin123
host: 127.0.0.1
virtual-host: /
@Configuration
public class RabbitmqCfg {
public final static String EXCHANGE_NAME = "fanout_order_exchange";
public final static String ROUTE_KEY = "";
// 声明一个fanout模式的交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME, true, false);
}
// 声明队列
@Bean
public Queue smsQueue() {
return new Queue("sms.fanout.queue", true);
}
@Bean
public Queue emailQueue() {
return new Queue("email.fanout.queue", true);
}
// 完成绑定关系
@Bean
public Binding smsBinding() {
return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
}
@Bean
public Binding emailBinding() {
return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
}
}
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public String makeOrder() {
String orderId = SimpleDateFormat.getDateTimeInstance().format(new Date()).concat(" - ").concat(UUID.randomUUID().toString());
System.out.println("Order保存逻辑处理");
rabbitTemplate.convertAndSend(RabbitmqCfg.EXCHANGE_NAME, RabbitmqCfg.ROUTE_KEY, orderId);
return "1";
}
}
@SpringBootTest
class Rabbitmq1ApplicationTests {
@Autowired
OrderService orderService;
@Test
void orderTest() {
String res = orderService.makeOrder();
System.out.println("res = " + res);
}
}
@RabbitListener(queues = {"email.fanout.queue"})
@Service
public class FanoutEmailConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("EMAIL FANOUT 接收到消息 --> " + msg);
}
}
@Service
public class FanoutSmsConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("SMS FANOUT 接收到消息 --> " + msg);
}
}
启动测试,查看控制台输出:
rabbitConnectionFactory#46039a21:0/SimpleConnection@73c31181 [delegate=amqp://admin@127.0.0.1:5672/, localPort= 3030]
SMS FANOUT 接收到消息 --> 2023-3-23 12:39:46 - bdecc460-3507-4f97-a104-b721dfbfabaa
EMAIL FANOUT 接收到消息 --> 2023-3-23 12:39:46 - bdecc460-3507-4f97-a104-b721dfbfabaa
EMAIL FANOUT 接收到消息 --> 2023-3-23 14:07:46 - 5f6e8940-4ecb-4d0b-8058-ae47844af5d7
SMS FANOUT 接收到消息 --> 2023-3-23 14:07:46 - 5f6e8940-4ecb-4d0b-8058-ae47844af5d7
@Configuration
public class DirectRabbitmqCfg {
public final static String EXCHANGE_NAME = "direct_order_exchange";
// 声明一个direct模式的交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange(EXCHANGE_NAME, true, false);
}
// 声明队列
@Bean
public Queue directSmsQueue() {
return new Queue("sms.direct.queue", true);
}
@Bean
public Queue directEmailQueue() {
return new Queue("email.direct.queue", true);
}
// 完成绑定关系
@Bean
public Binding directSmsBinding() {
return BindingBuilder.bind(directSmsQueue()).to(directExchange()).with("sms");
}
@Bean
public Binding directEmailBinding() {
return BindingBuilder.bind(directEmailQueue()).to(directExchange()).with("email");
}
}
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public String makeOrderDirect() {
String orderId = SimpleDateFormat.getDateTimeInstance().format(new Date()).concat(" - ").concat(UUID.randomUUID().toString());
System.out.println("Order保存逻辑处理");
rabbitTemplate.convertAndSend(DirectRabbitmqCfg.EXCHANGE_NAME, "sms", orderId);
return "1";
}
}
@Test
void orderDirectMQTest(){
String res = orderService.makeOrderDirect();
System.out.println("res = " + res);
}
@RabbitListener(queues = {"email.direct.queue"})
@Service
public class DirectEmailConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("EMAIL direct 接收到消息 --> " + msg);
}
}
@RabbitListener(queues = {"sms.direct.queue"})
@Service
public class DirectSmsConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("SMS direct 接收到消息 --> " + msg);
}
}
启动测试输出:
2023-03-23 14:30:09.211 INFO 13160 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#33a55bd8:0/SimpleConnection@3f63a513 [delegate=amqp://admin@127.0.0.1:5672/, localPort= 3643]
SMS direct 接收到消息 --> 2023-3-23 14:29:54 - 9b5b8678-81d2-407a-b41b-783f4a5fe7df
问题:消费者和生产者都可以对消息交换机、队列、关系进行配置,那么在生产者配置好,还是消费者更好?
我们可以在web管理页面、生产者、消费者端进行交换机、队列、关系的配置。但是还是消费者来定义比较好,一般消费者是最先启动的服务,如果没有的话,消费者端会启动失败。
@Service
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "email.topic.queue",declare = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic-order-exchange",type = ExchangeTypes.TOPIC),
key = "*.email.#"
))
public class TopicEmailConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("Email Topic 接收到消息 --> " + msg);
}
}
@Service
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "sms.topic.queue",declare = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic-order-exchange",type = ExchangeTypes.TOPIC),
key = "#.sms.#"
))
public class TopicSmsConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("SMS Topic 接收到消息 --> " + msg);
}
}
@Service
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "tel.topic.queue",declare = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic-order-exchange",type = ExchangeTypes.TOPIC),
key = "com.#"
))
public class TopicTelConsumer {
@RabbitHandler
public void receiveMsg(String msg) {
System.out.println("TEL Topic 接收到消息 --> " + msg);
}
}
public String makeOrderTopic(String routeKey) {
String orderId = SimpleDateFormat.getDateTimeInstance().format(new Date()).concat(" - ").concat(UUID.randomUUID().toString());
System.out.println("Order保存逻辑处理");
rabbitTemplate.convertAndSend("topic-order-exchange", routeKey, orderId);
return "1";
}
/**
* #.sms.#
* *.email.#
* tel : com.#
*/
@Test
void orderDirectMQTopic_1() {
String res = orderService.makeOrderTopic("com.hi.email");
System.out.println("res = " + res);
}
@Test
void orderDirectMQTopic_2() {
String res = orderService.makeOrderTopic("com.sms");
System.out.println("res = " + res);
}
@Test
void orderDirectMQTopic_3() {
String res = orderService.makeOrderTopic("com.sms");
System.out.println("res = " + res);
}