RabbitMQ学习及SpringBoot集成RabbitMQ
一丶消息队列
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);
}
}
两个消费者都能监听到消息
三种常见交换机模型
-
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
消费者配置文件只有端口不同
二丶Direct(直连型交换机)交换机模型
固定RoutingKey
当有多个消费者监听队列时,消息会轮询消费,不会出现消息重复消费现象
生产者
1. 配置类
声明交换机,声明队列,队列绑定交换机声明RoutingKey
/**
* @author Wcj
* @description: rabbitmq直连型交换机配置文件
* @date 2021/7/15 10:59
*/
2. 发送消息
使用springboot提供的RabbitTemplate发送消息
@Autowired private RabbitTemplate rabbitTemplate;
/**
* 直连型交换机发送消息
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
消费者
创建RabbitMq监听类
/**
* @author Wcj
* @description: 监听直连型模式消息的监听类
* @date 2021/7/15 14:35
*/
三丶Topic交换机模型
匹配RoutingKey规则的交换机
生产者
1.配置类
声明交换机,声明队列,队列绑定交换机声明RoutingKey
/**
* @author Wcj
* @description: Topic交换机配置类
* @date 2021/7/15 15:05
*/
2.发送消息
/**
* topic模式发送消息到routingkey为topic.man的交换机中
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
消费者
创建Rabbitmq监听类
/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
只能监听到topic.man开头的消息
/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
两条topic开头的消息都能监听到
四丶Fanout(扇形交换机)交换机模型
无RoutingKey绑定
生产者
1.配置类
/**
* @author Wcj
* @description: 扇型交换机的配置类
* @date 2021/7/15 16:23
*/
2.发送消息
/**
* fanout发送消息到fanout开头的交换机中
* fanout模式不需要指定rontingkey,指定也不会生效
*
* @author Wcj
* @date: 2021/7/15 16:38
*/
交换机会给三个队列都发送消息,不需要匹配routingKey
消费者
这里有三个消费者,只演示一个
/**
* @author Wcj
* @description: 监听fanout模式队列为fanout_queue_one消息的监听类
* @date 2021/7/15 14:35
*/
三个消费者分开监听不同的队列,分别接收到同一个交换机发出的消息
五丶生产者消息回调确认机制
1.在生产者的配置文件中加入配置
#确认消息已发送到交换机
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
server
2.配置相关的回调函数
/**
* @author Wcj
* @description: rabbitmq配置文件
* @date 2021/7/15 17:04
*/
我们在上面写了两个回调函数一个叫:ConfirmCallback ,一个叫RetrunCallback
测试什么情况下会触发上面两种回调函数
-
消息推送到server,但是在server里找不到交换机
-
消息推送到server,找到交换机了,但是没找到队列
-
消息推送到sever,交换机和队列啥都没找到
-
消息推送成功
①丶测试交换机不存在的情况
编写测试接口,将消息发送到不存在的交换机中(non-existent-exchange)
/**
* 生产者自动回调测试
* 测试不存在的交换机和队列自动调用回调函数
*
* @author Wcj
* @date: 2021/7/15 17:11
*/
结论:调用接口会回调用ConfirmCallback函数
②丶消息推送到交换机但是没找到队列情况
只声明交换机,不绑定任何队列
/**
* @author Wcj
* @description: 测试交换机存在,队列不存在的生产者异常情况
* @date 2021/7/15 17:22
*/
/**
* 生产者自动回调测试
* 测试不存在的队列自动调用回调函数
*
* @author Wcj
* @date: 2021/7/15 17:11
*/
结论:这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数
③丶消息推送到sever,交换机和队列都找不到
结论:这种情况和第一种情况是一样的,会直接调用ConfirmCallback函数
④丶消息发送成功
结论:会调用ConfirmCallback函数
六丶消费者消息回调确认机制
消费者消息确认机制和生产者确认机制不同,因为消费者在监听消息的同时也是在确认消息,所以消费者确认机制分为三种
1.自动确认:
这也是默认的消息确认情况。 AcknowledgeMode.NONE RabbitMQ成功将消息发出(即将消息成功写入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
*/