MQ学习
一丶消息队列
MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
二丶为什么要使用MQ
1丶系统解耦
如图:
系统未解耦前
系统解耦后
2丶异步调用
异步调用前
异步调用后
3丶流量削峰
流量削峰前
流量削峰后
三丶项目引入MQ的缺点
1丶系统可用性降低:
系统引入的外部依赖越多,系统要面对的风险越高,拿场景一来说,本来ABCD四个系统配合的好好的,没啥问题,但是你偏要弄个MQ进来插一脚,虽然好处挺多,但是万一MQ挂掉了呢,那样你系统不也就挂掉了。
2丶系统复杂程度提高:
非要加个MQ进来,如何保证没有重复消费呢?如何处理消息丢失的情况?怎么保证消息传递的顺序?问题太多
3丶一致性的问题:
A系统处理完再传递给MQ就直接返回成功了,用户以为你这个请求成功了,但是,如果在BCD的系统里,BC两个系统写库成功,D系统写库失败了怎么办,这样就导致数据不一致了。
所以。消息队列其实是一套非常复杂的架构,你在享受MQ带来的好处的同时,也要做各种技术方案把MQ带来的一系列的问题解决掉,等一切都做好之后,系统的复杂程度硬生生提高了一个等级。
RabbitMQ学习
一丶RabbitMQ流程图
组成部分说明
-
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue
-
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
-
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的
-
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送
-
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
生产者发送消息流程:
-
生产者和Broker建立TCP连接。
-
生产者和Broker建立通道。
-
生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
-
Exchange将消息转发到指定的Queue(队列)
消费者接收消息流程:
-
消费者和Broker建立TCP连接
-
消费者和Broker建立通道
-
消费者监听指定的Queue(队列)
-
当有消息到达Queue时Broker默认将消息推送给消费者。
-
消费者接收到消息。
-
ack回复
二丶六种消息模型
1丶基本消息模型:
-
P:生产者,也就是要发送消息的程序
-
C:消费者:消息的接受者,会一直等待消息到来。
-
queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
生产者
-
新建一个maven工程,添加amqp-client依赖
<!-- rabbitmq依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
<!-- spring的核心依赖,用于读取配置文件 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
-
连接工具类
-
配置文件
rabbitmqConfig.properties
## rabbitmq配置文件
host=localhost
port=5672
virtualHost=/
username=guest
password=guest
-
读取配置文件工具类
CommonUtil
public class CommonUtil {
/**
* 获取resources下的propertes文件,读取配置
*
* @author Wcj
* @date: 2021/7/9 10:59
*/
public static Map<String, String> getProperties() throws UnsupportedEncodingException {
try {
Properties pro = PropertiesLoaderUtils.loadAllProperties("rabbitmqConfig.properties");
Map<String, String> result = new HashMap<>();
Set<Map.Entry<Object, Object>> entries = pro.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
String key =(String) entry.getKey();
String value =(String)entry.getValue();
value = URLEncoder.encode(value, "ISO-8859-1");
value = URLDecoder.decode(value, "GBK");
result.put(key,value);
}
return result;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
-
rabbitmq连接工具类
public class ConnectionUtil {
/**
* 获取rabbitmq连接
* @author Wcj
* @date: 2021/7/9 16:13
*/
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
Map<String, String> properties = CommonUtil.getProperties();
String host = properties.get("host");
String port = properties.get("port");
String virtualHost = properties.get("virtualHost");
String username = properties.get("username");
String password = properties.get("password");
factory.setHost(host);
//端口
factory.setPort(Integer.parseInt(port));
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost(virtualHost);//设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机就相当于一个独立的mq
factory.setUsername(username);
factory.setPassword(password);
Connection connection = factory.newConnection();
return connection;
}
}
-
生产者发送消息
public class SimpleProducer {
private static final String SIMPLE_FILE_NAME = "simpleQueue";
public static void main(String[] args) throws IOException, TimeoutException {
try(// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 从连接中获取通道
Channel channel = connection.createChannel();){
// 3、声明(创建)队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(SIMPLE_FILE_NAME,false,false,false,null);
String message = "简单消息模式发送消息内容:RabbitMQ冲冲冲!!!======>";
System.out.println("生产者生产了消息:"+ message);
// 向指定的队列中发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props,消息的属性
* 4、body,消息内容
*/
channel.basicPublish("",SIMPLE_FILE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
消费者
/**
* @author Wcj
* @description: 简单消息模式消费者一
* 正常消费消息,消息自动ACK,消费消息正常
* @date 2021/7/9 16:03
*/
public class SimpleConsumerOne {
private static final String SIMPLE_FILE_NAME = "simpleQueue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(SIMPLE_FILE_NAME, false, false, false, null);
// 实现消费的方法
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
System.out.println("交换机名称为:" + exchange);
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
System.out.println("消息id为:" + deliveryTag);
// body 即消息体
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:" + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(SIMPLE_FILE_NAME, true, consumer);
}
}
演示消费者发送异常的时候消费者怎么处理
/**
* @author Wcj
* @description: 简单消息模式消费者二
* 消费者发生异常,手动进行消息ACk,消息未消费
* @date 2021/7/9 16:03
*/
public class SimpleConsumerTwo {
private static final String SIMPLE_FILE_NAME = "simpleQueue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(SIMPLE_FILE_NAME, false, false, false, null);
// 实现消费的方法
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 演示消费者消息接受失败,消息不会被消费
int i = 1 / 0;
//交换机
String exchange = envelope.getExchange();
System.out.println("交换机名称为:{}" + exchange);
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
System.out.println("消息id为:{}" + deliveryTag);
// body 即消息体
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:{}" + msg);
// 手动进行ACK
channel.basicAck(deliveryTag, false);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(SIMPLE_FILE_NAME, false, consumer);
}
}
2丶Work消息模型
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。
这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。
接下来我们来模拟这个流程:
P:生产者:任务的发布者
C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)
C2:消费者2:领取任务并且完成任务,假设完成速度较快
生产者
/**
* @author Wcj
* @description:竞争工作模式生产者
* @date 2021/7/12 10:46
*/
public class ContendWordProducer {
// 循环生产50条数据
private static final String WORK_QUEUE = "workQueue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(WORK_QUEUE,false,false,false,null);
// 循环发送五十条信息
for (int i = 1; i <= 50; i++) {
StringBuilder builder = new StringBuilder("竞争工作队列消息");
builder.append(i);
String message = builder.toString();
channel.basicPublish("",WORK_QUEUE,null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
消费者
通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理1个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。相反,它会将其分派给不是仍然忙碌的下一个Consumer。
值得注意的是:prefetchCount在手动ack的情况下才生效,自动ack不生效。
package com.zzzwww.rabbitmq.contendwordmode;
import com.rabbitmq.client.*;
import com.zzzwww.utils.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
/**
* @author Wcj
* @description: 竞争工作消息模式消费者一
* 正常消费消息,消息自动ACK,消费消息正常
* @date 2021/7/9 16:03
*/
public class ContennnndWordConsumerOne {
private static final String WORK_QUEUE = "workQueue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(WORK_QUEUE, false, false, false, null);
// 设置每个消费者同时只能处理一条消息,在手动ACK下才生效
channel.basicQos(1);
// 实现消费的方法
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:" + msg);
channel.basicAck(deliveryTag,false);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(WORK_QUEUE, false, consumer);
}
}
订阅模型分类
1、一个生产者多个消费者 2、每个消费者都有一个自己的队列 3、生产者没有将消息直接发送给队列,而是发送给exchange(交换机、转发器) 4、每个队列都需要绑定到交换机上 5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者消费 例子:注册->发邮件、发短信
X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange类型有以下几种:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。
3丶Publish/subscribe
(交换机类型:Fanout,也称为广播 )
生产者
-
生产者不需要声明queue
-
生产者声明Exchange,消息直接发送到Exchange
/**
* @author Wcj
* @description: 广播类型的生产者
* @date 2021/7/12 15:54
*/
public class FanoutExchangeProducer {
private static final String FANOUT_EXCHANGE = "fanoutExchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机,指定类型为fanout
channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
String message = "广播模式发送消息,冲冲冲!!!=======>";
/**
* 交换机名称
* 路由键
* 消息的属性
* 消息的内容
*/
channel.basicPublish(FANOUT_EXCHANGE,"",null,message.getBytes());
System.out.println(message);
channel.close();
connection.close();
}
}
消费者
/**
* @author Wcj
* @description: 广播类型的消费者
* @date 2021/7/12 16:19
*/
public class FanoutExchangeConsumer {
private static final String FANOUT_EXCHANGE = "fanoutExchange";
private static final String FANOUT_QUEUE_ONE = "faoutQueueOne";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(FANOUT_QUEUE_ONE,false,false,false,null);
// 绑定队列交换机
channel.queueBind(FANOUT_QUEUE_ONE,FANOUT_EXCHANGE,"");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(FANOUT_QUEUE_ONE,true,consumer);
}
}
4丶Routing 路由模型
交换机类型:direct,也叫直连型交换机
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 sms的消息
生产者
/**
* @author Wcj
* @description: routing模式消息生产者
* @date 2021/7/14 14:42
*/
public class RoutingExchangeProducer {
private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";
private static final String ROUTING_KEY = "sms";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(ROUTING_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String message = "routing模式发送消息冲冲冲!!!====>";
// 发送消息指定routingkey为sms的routingkey才能接收到信息
channel.basicPublish(ROUTING_EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
System.out.println(message);
channel.close();;
connection.close();
}
}
消费者一
/**
* @author Wcj
* @description: routing模式消息消费者一
* @date 2021/7/14 15:00
*/
public class RoutingExchangeConsumerOne {
private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";
private static final String ROUTING_QUEUE_NAME = "routing_queue_name";
private static final String ROUTING_KEY = "sms";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(ROUTING_QUEUE_NAME,false,false,false,null);
// 队列绑定交换机指定routingkey
channel.queueBind(ROUTING_QUEUE_NAME,ROUTING_EXCHANGE_NAME,ROUTING_KEY);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(message);
}
};
// 监听队列,自动ACK
channel.basicConsume(ROUTING_QUEUE_NAME,true,consumer);
}
}
消费者二
/**
* @author Wcj
* @description: routing模式消息消费者一
* @date 2021/7/14 15:00
*/
public class RoutingExchangeConsumerTwo {
private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";
private static final String ROUTING_QUEUE_NAME = "routing_queue_name";
private static final String ROUTING_KEY = "email";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(ROUTING_QUEUE_NAME,false,false,false,null);
// 队列绑定交换机指定routingkey
channel.queueBind(ROUTING_QUEUE_NAME,ROUTING_EXCHANGE_NAME,ROUTING_KEY);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(message);
}
};
// 监听队列,自动ACK
channel.basicConsume(ROUTING_QUEUE_NAME,true,consumer);
}
}
只有消费者一才能收到消息,因为消费者一是监听Routingkey为sms的队列
5丶Topics 通配符模式
交换机类型:topics
-
每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。
-
Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:topic.man
通配符规则:
-
#:匹配一个或多个词
-
*:匹配不多不少恰好1个词
生产者
/**
* @author Wcj
* @description: Topic通配符模式消息生产者
* @date 2021/7/14 15:36
*/
public class TopicExchangeProducer {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词
private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";
private static final String ROUTING_KEY = "zw.sms";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String message = "这是给zw系统发送的sms消息===>";
channel.basicPublish(TOPIC_EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
System.out.println(message);
channel.close();
connection.close();
}
}
消费者一
/**
* @author Wcj
* @description: Topic通配符模式消息消费者一
* 监听zw下的服务
* @date 2021/7/14 15:36
*/
public class TopicExchangeConsumerOne {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词
private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";
private static final String TOPIC_QUEUE_NAME = "topic_queue_name";
private static final String ROUTING_KEY = "zw.*";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TOPIC_QUEUE_NAME,false,false,false,null);
channel.queueBind(TOPIC_QUEUE_NAME,TOPIC_EXCHANGE_NAME,ROUTING_KEY);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:" + msg);
}
};
channel.basicConsume(TOPIC_QUEUE_NAME,true,consumer);
}
}
消费者二
/**
* @author Wcj
* @description: Topic通配符模式消息消费者二
* 监听sms的消息
* @date 2021/7/14 15:36
*/
public class TopicExchangeConsumerTwo {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词
private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";
private static final String TOPIC_QUEUE_NAME = "topic_queue_name";
private static final String ROUTING_KEY = "*.sms";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TOPIC_QUEUE_NAME,false,false,false,null);
channel.queueBind(TOPIC_QUEUE_NAME,TOPIC_EXCHANGE_NAME,ROUTING_KEY);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容为:" + msg);
}
};
channel.basicConsume(TOPIC_QUEUE_NAME,true,consumer);
}
}
两个消费者都能监听到消息
SpringBoot集成RabbitMQ学习
三种常见交换机模型
-
Direct Exchange
直连型交换机,根据消息携带的路由键将消息投递给对应队列。
大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。 然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
-
Fanout Exchange
扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
-
Topic Exchange
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。 简单地介绍下规则: * (星号) 用来表示一个单词 (必须出现的) # (井号) 用来表示任意数量(零个或多个)单词 通配的绑定键是跟队列进行绑定的
一丶项目准备
-
创建两个项目,一个生产者springboot-rabbitmq-producer,一个消费者spring-rabbitmq-consumer-
-
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
-
配置文件
server:
port: 10096
spring:
application:
name: mq-rabbitmq-producer
# 配置rabbirmq服务器
rabbitmq:
host: localhost
port: 5672
username: root
password: root
# 虚拟host,可以不设置,使用server默认host
virtualHost: /zzzwww
template:
retry:
enabled: true
initial-interval: 10000ms
max-interval: 300000ms
multiplier: 2
消费者配置文件只有端口不同
二丶Direct(直连型交换机)交换机模型
固定RoutingKey
当有多个消费者监听队列时,消息会轮询消费,不会出现消息重复消费现象
生产者
1. 配置类
声明交换机,声明队列,队列绑定交换机声明RoutingKey
/**
* @author Wcj
* @description: rabbitmq直连型交换机配置文件
* @date 2021/7/15 10:59
*/
@Configuration
public class DirectRabbitConfig {
// 直连型队列名称
public static final String QUEUE_NAME = "direct_queue";
// 直连型交换机名称
public static final String EXCHANGE_NAME = "direct_exchange";
// 直连交换机的routingkey
public static final String ROUTING_KEY = "direct_routing_key";
/**
* 声明队列
* @author Wcj
* @date: 2021/7/15 11:08
*/
@Bean()
public Queue DirectQueue(){
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
// 一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue(QUEUE_NAME,true);
}
/**
* 声明交换机
* @author Wcj
* @date: 2021/7/15 11:11
*/
@Bean
public DirectExchange DirectExchange(){
return new DirectExchange(EXCHANGE_NAME,true,false);
}
/**
* 将队列和交换机绑定,并设置匹配键:direct_routing_key
* @author Wcj
* @date: 2021/7/15 11:15
*/
@Bean
public Binding bindDirect(){
return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with(ROUTING_KEY);
}
}
2. 发送消息
使用springboot提供的RabbitTemplate发送消息
@Autowired private RabbitTemplate rabbitTemplate;
/**
* 直连型交换机发送消息
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
@GetMapping("/directMessage")
public String sendDirectMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 将消息携带绑定键值:direct_routing_key,发送到交换机:EXCHANGE_NAME
Map<String, String> map = new HashMap<>();
map.put("messageId", messageId);
map.put("createTime", createTime);
String message = "直连类型的交换机模式消息";
map.put("messageData", message);
rabbitTemplate.convertAndSend(DirectRabbitConfig.EXCHANGE_NAME, DirectRabbitConfig.ROUTING_KEY, map);
return "success";
}
消费者
创建RabbitMq监听类
/**
* @author Wcj
* @description: 监听直连型模式消息的监听类
* @date 2021/7/15 14:35
*/
@Component
@RabbitListener(queues = "direct_queue")
public class DirectReceiverOne {
@RabbitHandler
public void process(Map message){
System.out.println("DirectReceiverOne消费者收到消息:"+message.toString());
}
}
三丶Topic交换机模型
匹配RoutingKey规则的交换机
生产者
1.配置类
声明交换机,声明队列,队列绑定交换机声明RoutingKey
/**
* @author Wcj
* @description: Topic交换机配置类
* @date 2021/7/15 15:05
*/
@Configuration
public class TopicRabbitConfig {
// routingkey
public static final String MAN = "topic.man";
// routingkey
public static final String PERSON = "topic.*";
public static final String TOPIC_QUEUE_MAN = "topic_queue_man";
public static final String TOPIC_QUEUE = "topic_queue";
public static final String TOPIC_EXCHANGE = "topic_exchange";
// 声明队列
@Bean
public Queue topicManQueue(){
return new Queue(TOPIC_QUEUE_MAN);
}
// 声明队列
@Bean
public Queue topicQueue(){
return new Queue(TOPIC_QUEUE);
}
// 声明交换机
@Bean
public Exchange topicExchange(){
return new TopicExchange(TOPIC_EXCHANGE);
}
// topic_queue队列绑定topic_exchange交换机,绑定的键为topic.man
// 这样只要是消息携带的路由键是topic.woman,才会分发到该队列
@Bean
public Binding bindingExchangeMessage(){
return BindingBuilder.bind(topicManQueue()).to(topicExchange()).with(MAN).noargs();
}
// topic_queue队列绑定topic_exchange交换机,绑定的键为topic.man
// 这样只要是消息携带的路由键是topic.man,才会分发到该队列
@Bean
public Binding bindingExchangeMessage2(){
return BindingBuilder.bind(topicQueue()).to(topicExchange()).with(PERSON).noargs();
}
}
2.发送消息
/**
* topic模式发送消息到routingkey为topic.man的交换机中
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
@GetMapping("/topicMessageMan")
public String topicMessageMan() {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, String> map = new HashMap<>();
map.put("messageId", messageId);
map.put("createTime", createTime);
String message = "topic类型的交换机模式消息";
map.put("messageData", message);
// 发送消息给routing为man的队列
rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.MAN, map);
return "success";
}
/**
* topic模式发送消息到routingkey为topic.开头的交换机中
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
@GetMapping("/topicMessagePerson")
public String topicMessagePerson() {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, String> map = new HashMap<>();
map.put("messageId", messageId);
map.put("createTime", createTime);
String message = "topic类型的交换机模式消息";
map.put("messageData", message);
// 发送消息给routing为topic开头的队列
rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, "topic.zhangsan", map);
return "success";
}
消费者
创建Rabbitmq监听类
/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
@Component
@RabbitListener(queues = "topic_queue_man")
public class TopicReceviverOne {
@RabbitHandler
public void process(Map topicMessage){
System.out.println("topic消费者监听到队列为topic_queue_man,routingkey为topic.man的消息:"+topicMessage.toString());
}
}
只能监听到topic.man开头的消息
/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
@Component
@RabbitListener(queues = "topic_queue")
public class TopicReceviverTwo {
@RabbitHandler
public void process(Map topicMessage){
System.out.println("topic消费者监听到队列为topic_queue,routingkey为topic开头的消息:"+topicMessage.toString());
}
}
两条topic开头的消息都能监听到
四丶Fanout(扇形交换机)交换机模型
无RoutingKey绑定
生产者
1.配置类
/**
* @author Wcj
* @description: 扇型交换机的配置类
* @date 2021/7/15 16:23
*/
@Configuration
public class FanoutRabbitConfig {
public static final String FANOUT_QUEUE_ONE = "fanout_queue_one";
public static final String FANOUT_QUEUE_TWO = "fanout_queue_two";
public static final String FANOUT_QUEUE_THREE = "fanout_queue_three";
public static final String FANOUT_EXCHANGE = "fanout_exchange";
/**
* 创建三个队列 :fanout.A fanout.B fanout.C
* 将三个队列都绑定在交换机 fanoutExchange 上
* 因为是扇型交换机, 路由键无需配置,配置也不起作用
*/
@Bean
public Queue fanoutQueueOne(){
return new Queue(FANOUT_QUEUE_ONE);
}
@Bean
public Queue fanoutQueueTwo(){
return new Queue(FANOUT_QUEUE_TWO);
}
@Bean
public Queue fanoutQueueThree(){
return new Queue(FANOUT_QUEUE_THREE);
}
@Bean
public Exchange fanoutExchange(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding fanoutExchangeQueueOne(){
return BindingBuilder.bind(fanoutQueueOne()).to(fanoutExchange()).with("").noargs();
}
@Bean
public Binding fanoutExchangeQueueTwo(){
return BindingBuilder.bind(fanoutQueueTwo()).to(fanoutExchange()).with("").noargs();
}
@Bean
public Binding fanoutExchangeQueueThree(){
return BindingBuilder.bind(fanoutQueueThree()).to(fanoutExchange()).with("").noargs();
}
}
2.发送消息
/**
* fanout发送消息到fanout开头的交换机中
* fanout模式不需要指定rontingkey,指定也不会生效
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
@GetMapping("/fanoutMessage")
public String fanoutMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, String> map = new HashMap<>();
map.put("messageId", messageId);
map.put("createTime", createTime);
String message = "fanout类型的交换机模式消息";
map.put("messageData", message);
// 发送消息给routing为topic开头的队列
rabbitTemplate.convertAndSend(FanoutRabbitConfig.FANOUT_EXCHANGE, "", map);
return "success";
}
交换机会给三个队列都发送消息,不需要匹配routingKey
消费者
这里有三个消费者,只演示一个
/**
* @author Wcj
* @description: 监听fanout模式队列为fanout_queue_one消息的监听类
* @date 2021/7/15 14:35
*/
@Component
@RabbitListener(queues = "fanout_queue_one")
public class FanoutReceiverOne {
@RabbitHandler
public void process(Map message){
System.out.println("FanoutReceiverOne消费者收到消息:"+message.toString());
}
}
三个消费者分开监听不同的队列,分别接收到同一个交换机发出的消息
五丶生产者消息回调确认机制
1.在生产者的配置文件中加入配置
#确认消息已发送到交换机
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
server:
port: 10096
spring:
application:
name: mq-rabbitmq-producer
# 配置rabbirmq服务器
rabbitmq:
host: localhost
port: 5672
username: root
password: root
# 虚拟host,可以不设置,使用server默认host
virtualHost: /zzzwww
# 确认消息已发送到交换机
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
template:
retry:
enabled: true
initial-interval: 10000ms
max-interval: 300000ms
multiplier: 2
2.配置相关的回调函数
/**
* @author Wcj
* @description: rabbitmq配置文件
* @date 2021/7/15 17:04
*/
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
}
});
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("ReturnCallback: "+"消息:"+returnedMessage.getMessage());
System.out.println("ReturnCallback: "+"回应码:"+returnedMessage.getReplyCode());
System.out.println("ReturnCallback: "+"回应信息:"+returnedMessage.getReplyText());
System.out.println("ReturnCallback: "+"交换机:"+returnedMessage.getExchange());
System.out.println("ReturnCallback: "+"路由键:"+returnedMessage.getRoutingKey());
}
});
return rabbitTemplate;
}
}
我们在上面写了两个回调函数一个叫:ConfirmCallback ,一个叫RetrunCallback
测试什么情况下会触发上面两种回调函数
-
消息推送到server,但是在server里找不到交换机
-
消息推送到server,找到交换机了,但是没找到队列
-
消息推送到sever,交换机和队列啥都没找到
-
消息推送成功
①丶测试交换机不存在的情况
编写测试接口,将消息发送到不存在的交换机中(non-existent-exchange)
/**
* 生产者自动回调测试
* 测试不存在的交换机和队列自动调用回调函数
*
* @author Wcj
* @date: 2021/7/15 17:11
*/
@GetMapping("/noExistMessageAck")
public String TestMessageAck() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: non-existent-exchange test message ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map);
return "ok";
}
结论:调用接口会回调用ConfirmCallback函数
②丶消息推送到交换机但是没找到队列情况
只声明交换机,不绑定任何队列
/**
* @author Wcj
* @description: 测试交换机存在,队列不存在的生产者异常情况
* @date 2021/7/15 17:22
*/
@Configuration
public class TestNoExistExchangeConfig {
public static final String EXIST_EXCHANGE = "exist_exchange";
// 只声明了交换机,没有绑定队列
@Bean
public Exchange existExchange(){
return new DirectExchange(EXIST_EXCHANGE);
}
}
/**
* 生产者自动回调测试
* 测试不存在的队列自动调用回调函数
*
* @author Wcj
* @date: 2021/7/15 17:11
*/
@GetMapping("/noExistQueueMessageAck")
public String noExistQueueMessageAck() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: non-existent-exchange test message ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend(TestNoExistExchangeConfig.EXIST_EXCHANGE, "TestDirectRouting", map);
return "ok";
}
结论:这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数
③丶消息推送到sever,交换机和队列都找不到
结论:这种情况和第一种情况是一样的,会直接调用ConfirmCallback函数
④丶消息发送成功
结论:会调用ConfirmCallback函数
六丶消费者消息回调确认机制
消费者消息确认机制和生产者确认机制不同,因为消费者在监听消息的同时也是在确认消息,所以消费者确认机制分为三种
1.自动确认:
这也是默认的消息确认情况。 AcknowledgeMode.NONERabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
2.手动确认
我们在配置接受消息确认机制时,常采用这种模式
-
basic.ack用于肯定确认
-
basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
-
basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。 而basic.nack,basic.reject表示没有被正确处理
-
消息重新入列场景需要用到Reject
channel.basicReject(deliveryTag, true); 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。
使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。
但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。
-
设置不消费某条消息的场景需要用到nack
channel.basicNack(deliveryTag, false, true); 第一个参数依然是当前消息到的数据的唯一id; 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
代码:
1. 在消费者创建一个手动确认消息监听类
/**
* @author Wcj
* @description:
* @date 2021/7/16 15:05
*/
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 因为传递消息的时候用的map传递,所以将Map从Message内取出需要做些处理
String msg = message.toString();
String[] msgArray = msg.split("'");//可以点进Message里面看源码,单引号直接的数据就是我们的map消息数据
Map<String, String> msgMap = mapStringToMap(msgArray[1].trim(),3);
String messageId=msgMap.get("messageId");
String messageData=msgMap.get("messageData");
String createTime=msgMap.get("createTime");
if ("direct_queue".equals(message.getMessageProperties().getConsumerQueue())){
System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
System.out.println("消息成功消费到 messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
System.out.println("执行TestDirectQueue中的消息的业务处理流程......");
}
if ("fanout_queue_one".equals(message.getMessageProperties().getConsumerQueue())){
System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
System.out.println("消息成功消费到 messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
System.out.println("执行fanout.A中的消息的业务处理流程......");
}
// 第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
channel.basicAck(deliveryTag, true);
// channel.basicReject(deliveryTag, true);
// 第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
} catch (Exception e) {
channel.basicReject(deliveryTag, false);
e.printStackTrace();
}
}
//{key=value,key=value,key=value} 格式转换成map
private Map<String, String> mapStringToMap(String str,int entryNum ) {
str = str.substring(1, str.length() - 1);
String[] strs = str.split(",",entryNum);
Map<String, String> map = new HashMap<String, String