RabbitMQ 入门学习
MQ 消息队列
MQ全称为Message Queue即消息队列
什么是消息队列?
"消息队列"
是在消息的传输过程中保存消息的容器。- 它是典型的:生产者————消费者模型
1.生产者不断向消息队列中生产消息
2.消费者不断的从队列中获取消息。
3.因为消息的生产和消费都是异步的,而且只关心消息的发送和接收。
4.没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
消息队列 常用场景:⭐
以下信息来源于:大佬,感谢大佬!!分享
服务解耦
场景
-
服务A产生数据, 而服务B,C,D需要这些数据
那么我们可以在A服务中直接调用B,C,D服务,把数据传递到下游服务即可 -
但 随着我们的应用规模不断扩大,会有更多的服务需要A的数据,
如果有几十甚至几百个下游服务,而且会不断变更,再加上还要考虑下游服务出错的情况,
那么,A服务中调用代码的维护会极为困难
MQ解耦
- A服务只需要向消息服务器发送消息,而不用考虑谁需要这些数据;
- 下游服务如果需要数据,自行从消息服务器订阅消息,不再需要数据时则取消订阅即可
流量削峰
场景:
- 假设我们有一个应用,平时访问量是每秒300请求,我们用一台服务器即可轻松应对
- 在高峰期, 访问量瞬间翻了十倍, 达到每秒3000次请求, 单台服务器无法应对, 这时我们增加到10台服务器,减压
- 但如果这种瞬时高峰的情况每天只出现一次,每次只有半小时
那么我们10台服务器在多数时间都只分担每秒几十次请求,这样就有点浪费资源了
MQ
- 使用RabbitMQ来进行流量削峰
高峰情况下, 瞬间出现的大量请求数据, 先发送到消息队列服务器, 排队等待被处理 - 我们的应用,可以慢慢的从消息队列接收请求数据进行处理
这样把数据处理时间拉长,以减轻瞬时压力
异步调用
场景:
- 外卖支付
支付后要发送支付成功的通知
再寻找外卖小哥来进行配送,而寻找外卖小哥的过程非常耗时 - 尤其是高峰期,可能要等待几十秒甚至更长,这样就造成整条调用链路响应非常缓慢
MQ - 订单数据可以发送到消息队列服务器
那么调用链路也就可以到此结束, 订单系统则可以立即得到响应, 整条链路的响应时间只有200毫秒左右; - 寻找外卖小哥的应用可以,
异步的方式从消息队列接收订单消息,再执行耗时的寻找操作
常见MQ产品
-
ActiveMQ:基于JMS
-
Kafka:分布式消息系统,高吞吐量
-
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
-
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
AMQP 和 JMS ⭐
-
AMQP
高级消息队列协议!
是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMSRabbitMQ 就是基于 AMQP 协议实现的。
-
JMS
java 消息服务
JMS的客户端之间可以通过JMS服务
进行异步的消息传输。
JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范
允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。
它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 ActiveMQ 就是基于 JMS 规范实现的。
总结:
-
AMQP 为消息定义了线路层(wire-level protocol)的协议 JMS所定义的是API规范。
-
跨平台:
Java 体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。
而AMQP天然具有跨平台、跨语言特性。
-
支持消息类型:
JMS 支持TextMessage、MapMessage 等复杂的消息类型;
而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送) -
Exchange
交换机
提供的路由算法
AMQP可以提供多样化的路由方式来传递消息到消息队列 4种交换机类型,6种模式
JMS 仅支持 队列 和 主题/订阅 方式两种
RabbitMQ 简介:
-
MQ全称为Message Queue即
消息队列
-
RabbitMQ是由
erlang
语言开发,所以安装环境需要安装erlang
-
基于
AMQP
(Advanced Message Queue 高级消息队列协议
)协议实现的消息队列 -
它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛
RabbitMQ 的工作原理
组成部分:⭐
-
Broker
消息队列服务进程
:此进程包括两个部分:Exchange交换机
和Queue队列
-
Exchange
交换机
:按一定的规则将消息路由转发到某个队列,对消息进行过虑。
一个有四种类型:Direct, Fanout, Topic, Headers -
Queue
消息队列
:存储消息的队列。 -
Producer 消息生产者,即生产方客户端,生产方客户端将消息发送到MQ
-
Consumer 消息消费者,即消费方客户端,接收MQ转发的消息。
-----发送消息-----
- 生产者和Broker建立TCP连接。
- 生产者和Broker建立通道。
- 生产者通过通道消息发送给Broker,由Exchange将消息进行转发~
队列中去!
----接收消息-----
- 消费者和Broker建立TCP连接
- 消费者和Broker建立通道
- 消费者监听指定的Queue(队列),当有消息到达Queue时Broker默认将消息推送给消费者。
Exchange交换机四种类型 ⭐
RabbitMQ消息传递模型的核心思想是:
生产者永远不会将任何消息直接发送到队列,通常生产者甚至不知道消息是否会被传递到任何队列。生产者只能向交换机(Exchange)发送消息。
交换机是一个非常简单的东西。一边接收来自生产者的消息,另一边将消息推送到队列。
- direct : 需要生产者和消费者绑定相同的Exchange和routing key
- fanout: 广播模式需要生产者消费者绑定相同的Exchange
- topic: 支持模糊匹配的广播模式以点分隔**
* 表示一个单词
# 表示任意数量(零个或多个)单词。 - header: 根据生产者和消费者的header中信息进行匹配,性能较差 ,x-match [all 匹配所有/any 任意一个]
RabbitMQ环境搭建 ✔
Erlang 环境配置:
- RabbitMQ由Erlang语言开发
所以需要安装对应的,运行环境!
- 找到 otp_win64_20.3.exe,以管理员方式运行此文件,安装
无脑下一步即可!
- 配置erlang环境变量
环境变量:ERLANG_HOME=erlang安装目录
注意别在中文目录下!
path中添加:%ERLANG_HOME%\bin
- 测试
RabbitMQ 安装:
-
rabbitmq-server-3.7.3.exe:以管理员方式运行此文件安装
下一步...
-
之后就可以在 windows 系统菜单看到:
-
install 安装服务
之后
start 启动服务即可… -
服务:安装/启动,ok 之后: 安装rabbitMQ的管理插件,
就可以通过浏览页面,方便的管理Rabbit了
安装目录下:管理员身份运行 rabbitmq-plugins.bat enable rabbitmq_management
-
进入浏览器,输入:
http://localhost:15672
用户/密码 默认: guest
-
主要端口介绍
4369 – erlang发现口
5672 – client端通信口
15672 – 管理界面ui端口
25672 – server间内部通信口
如果开始菜单中没有,服务则进入安装目录下sbin目录手动启动:
- 安装并运行服务:
rabbitmq-service.bat install 安装服务
bbitmq-service.bat start 启动服务
- 安装管理插件
如上
RabbitMQ 服务启动不…
- 这个我也遇到过,刚启动,就马上停止了! 后来以查
Windows账号名是中文的!!!
- 防不胜防!!中问到哪里都难受!!😒
如果你们也是这个问题,也可以试试我这个方法!
- 解决方案:
设置软连接,绕过中文名的目录
管理员运行cmd然后打开RabbitMQ安装目录
rabbitmq-service.bat remove #删除服务
set RABBITMQ_BASE=D:\WSMwork\RabbitMQ\data #设置:目录, 自行设置
#重新生产服务,安装管理插件!
rabbitmq-service.bat install
rabbitmq-plugins enable rabbitmq_management
启动服务
Java 集成 RabbitMQ 模式开发:6种+常见场景
准备工作
不同的人有,不同的写法不过大致相同: 这两天看了好几篇博客好几种写法...
- 添加依赖
- 实现代码 一个消息
发送者Producter
一个消息接收者Consumer
两者,也大致相同: 创建链接…创建通道…绑定.读/写
对于这种链接操作可以,创建一个工具类:util处理
依赖:
pom.xml
<!-- rabbitmq依赖! -->
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>
</dependencies>
RabbitMQ 连接工具类:
ConnectionUtil.Java
//注意MQ 的连接对象类型...
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
//RabbitMQ 连接配置类:
public class ConnectionUtil {
//静态方法,需要连接对象直接调用即可!
public static Connection getConnection() throws IOException, TimeoutException {
//获取一个链接工程
ConnectionFactory factory = new ConnectionFactory();
//设置属性
factory.setHost("127.0.0.1"); //ip
factory.setPort(5672); //端口
factory.setVirtualHost("/"); //虚拟主机地址,虚拟机相当于一个独立的mq服务器, /默认存在!
factory.setUsername("guest"); //用户密码
factory.setPassword("guest");
Connection connection = factory.newConnection();
return connection;
}
}
- 可以在RabbitMq 管理界面设置很多东西…
- 用户 设置权限,交换机,队列,
虚拟主机 就是一个存储位置..
查看交换机…
简单模式 “Hello World!”
如图,显而易见,非常简单就是一个一发一读
的过程…
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来。
- queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
- 简单模式:
不需要交换机!
发送者
HellowProducter.Java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.wsm.util.ConnectionUtil;
//发送者
public class HellowProducter {
//发送消息
public void send() throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
//创建通道!Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
Channel channel = connection.createChannel();
/**设置消息队列的属性!
* queue :队列名称
* durable :是否持久化 如果持久化,mq重启后队列数据还在! (队列是在虚拟路径上的...)
* exclusive :队列是否独占此连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* autoDelete :队列不再使用时是否自动删除此队列,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* arguments :队列参数 null,可以设置一个队列的扩展参数,需要时候使用!比如:可设置存活时间
* */
channel.queueDeclare("hello.queue", true, false, false, null);
//要发送的消息
String mess = "一条消息:" + new SimpleDateFormat("yyyy-MM-dd hh:mm-ss").format(new Date());
/**发送消息,参数:
* exchange :指定的交换机,不指定就会有默认的....
* routingKey :路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机routingKey设置为队列的名称
* props :消息包含的属性: 后面介绍,可以是一个一个对象...
* body :发送的消息,AMQP以字节方式传输...
* */
channel.basicPublish("", "hello.queue", null, mess.getBytes());
//关闭通道和连接(资源关闭最好用try-catch-finally语句处理
channel.close();
connection.close();
}
//main 启动运行...
public static void main(String[] args) throws Exception {
HellowProducter sp = new HellowProducter();
sp.send();
}
}
接收者
HelloConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
//消息接收者:
public class HelloConsumer {
public void revice() throws Exception {
//创建连接:
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello.queue", true, false, false, null);
//收到消息后用来处理消息的回调对象
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope: 可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//将返回的字节数组转换成 String 打印输出!
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复验证,这就是Unacked 为返回ack的数据
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume("hello.queue", true, defaultConsumer);
}
//main运行输出!
public static void main(String[] args)throws Exception {
HelloConsumer sc =new HelloConsumer();
sc.revice();
}
}
测试:
发送者
接收者:
- 可以看到…消息发送接收成功!!
消息确认接收机制(ACK)
-
消息一旦被消费者接收,队列中的消息就会被删除。
-
RabbitMQ怎么知道消息被接收了呢?
如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!
因此,RabbitMQ有一个ACK机制。
当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:
自动ACK: 消息一旦被接收,消费者自动发送ACK
手动ACK: 消息接收后,不会发送ACK,需要手动调用 -
使用场景:
如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK
工作模式 Work queues
- 工作模式 相当于 简单模式的
升级版!
多个消费者,对应一个发送者,发送者 产生的消息存在队列种,队列会以复杂均衡形式
轮询的发送给多个消费者 - 一般应用于:发送方事务简单,接收方事务复杂…
美团外卖:用户下单——后台内部要联系商家 骑手 生产订单 处理...
发送者:
HellowProducter.Java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
//发送者
public class WorkProducter {
//发送消息
public void send() throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
//创建通道!Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
Channel channel = connection.createChannel();
/**设置消息队列的属性!
* queue :队列名称
* durable :是否持久化 如果持久化,mq重启后队列数据还在! (队列是在虚拟路径上的...)
* exclusive :队列是否独占此连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* autoDelete :队列不再使用时是否自动删除此队列,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* arguments :队列参数 null,可以设置一个队列的扩展参数,需要时候使用!比如:可设置存活时间
* */
channel.queueDeclare("work.queue", true, false, false, null);
//要发送的消息
String mess = "一条消息:" + new SimpleDateFormat("yyyy-MM-dd hh:mm-ss").format(new Date());
/**发送消息,参数:
* exchange :指定的交换机,不指定就会有默认的....
* routingKey :路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机routingKey设置为队列的名称
* props :消息包含的属性: 后面介绍,可以是一个一个对象...
* body :发送的消息,AMQP以字节方式传输...
* */
channel.basicPublish("", "work.queue", null, mess.getBytes());
//关闭通道和连接(资源关闭最好用try-catch-finally语句处理
channel.close();
connection.close();
}
//main 启动运行...
public static void main(String[] args) throws Exception {
WorkProducter sp = new WorkProducter();
sp.send();
}
}
接收者:两个接收者代码一模一样!!
WorkConsumer2.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
//消息接收者:
public class WorkConsumer2 {
public void revice() throws Exception {
//创建连接:
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work.queue", true, false, false, null);
//收到消息后用来处理消息的回调对象
//basicConsume方法监听到数据后就会执行这个 handleDelivery 回调方法,进行数据展示处理!
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//将返回的字节数组转换成 String 打印输出!
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复验证,这就是Unacked 为返回ack的数据
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume("work.queue", true, defaultConsumer);
}
//main运行输出!
public static void main(String[] args)throws Exception {
WorkConsumer2 sc =new WorkConsumer2();
sc.revice();
}
}
测试:
发布订阅模式 Publish/Subscribe 交换机类型:Fanout
- 发布订阅:
可以理解为: 某某软件很多人关注/订阅了一个博主,博主一更新,所有的粉丝都收到更新消息!
所有的接收者,可以获得所有的发送者消息!
- 定义一个交换机:
fanout类型
Fanout也称为 广播模式
发送者:
PublishProducter.Java
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PublishProducter {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/**声明交换机
* 交换机名
* 交换机类型 FANOUT
* */
channel.exchangeDeclare("publish.exchange", BuiltinExchangeType.FANOUT);
/**声明队列/
channel.queueDeclare("email.queue", true, false, false, null);
/**交换机绑定队列! 发送者可以省略,发送者只要知道往那个交换机上发送即可!设置不需要知道队列!!
*指定的队列,与指定的交换机关联起来
*指定交换机
*第三个参数时 routingKey, 由于是fanout交换机, 这里忽略 routingKey; 但它是必须参数所以必须要加...而且无论加不加都不会产生影响!
*/
// channel.queueDeclare("email.queue", true, false, false, null);
// channel.queueDeclare("sms.queue", true, false, false, null);
//对于 fanout 类型的交换机,routingKey会被忽略,但不允许null值,允许 ""
// channel.queueBind("email.queue", "publish.exchange", "");
// channel.queueBind("sms.queue", "publish.exchange", "");
//发送的消息
String mess = "email和sms共同的一条消息:" + new SimpleDateFormat("yyyy-MM-dd hh:mm-ss").format(new Date());
//发送消息: 到交换机上,交换机根据 FANOUT类型给,每一个队列发送消息...
channel.basicPublish("publish.exchange", "", null, mess.getBytes());
connection.close();
}
}
消费者:
Email EmailConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class EmailConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare("publish.exchange", BuiltinExchangeType.FANOUT);
//绑定交换机队列
//自动生成对列名, 非持久,独占,自动删除
// String queuename = channel.queueDeclare().getQueue();
//绑定!
// channel.queueBind(queuename, "publish.exchange", "");
//或指定队列绑定!当然在这之前要存在声明!
channel.queueBind("email.queue", "publish.exchange", "");
//消息回调
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
//设置...
channel.basicConsume("email.queue",true,defaultConsumer);
}
}
SMS SMSConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class SMSConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("publish.exchange", BuiltinExchangeType.FANOUT);
channel.queueBind("sms.queue", "publish.exchange", "");
//消息回调
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
//设置...
channel.basicConsume("sms.queue",true,defaultConsumer);
}
}
测试:
- 两个接收者消息一模一样!
注意:
生产者永远不会将任何消息直接发送到队列,通常生产者甚至不知道消息是否会被传递到任何队列。
生产者只能向交换机(Exchange)发送消息。 所以生产者,可以不用指定/声明队列!
只需要在,消费者声明 绑定数据队列即可! 当然也可以使用程序:
生成对列名绑定;
自动生成对列名, 非持久,独占,自动删除 String queuename = channel.queueDeclare().getQueue();
绑定 channel.queueBind(queuename, "publish.exchange", "");
publish/subscribe
与work queues
有什么区别
区别:
- work queues不用定义交换机,而publish/subscribe 需要定义交换机。
- publish/subscribe的 生产方是面向交换机发送消息
work queues的生产方是面向队列发送消息(底层使用默认交换机)。 - publish/subscribe 需要设置队列和交换机的绑定
work queues不需要设置,实际上work queues会将队列绑定到默认的交换机 。
相同点:
- 所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。
- 建议使用 publish/subscribe
发布订阅模式可以指定自己专用的交换机
路由模式 Routing 交换机类型:DIRECT
-
绑定
交换机/队列
时候,需要指定 routing key 一个队列,可以设置多个 routingkey; -
发送者发送数据, 根据 routingkey 指定发送消息的类型,
-
接收者,通过 绑定交换机 + 队列 +
routing key
来匹配, 确定消息发送到那个队列上! -
交换机类型:
direct
向 direct 类型,交换机,发送数据必须携带,Routing key
发送者
RoutingProducter.Java
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeoutException;
public class RoutingProducter {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//指定交换机 及 交换机的类型
channel.exchangeDeclare("rout.exchange", BuiltinExchangeType.DIRECT);
// 定义队列.....发送者可以不需要!
// channel.queueDeclare("rout.queue", true, false, false, null);
//要发送的信息!
String mess = "一条消息:" + new SimpleDateFormat("yyyy-MM-dd hh:mm-ss").format(new Date());
//开始发送... 发送的交换机 发送的key队列 发送的特殊类型数据 发送的方式字节数组!
//发送数据时候指定 routingkey 取数据时候,根据指定的routingkey 而获得对应的routingkey 匹配的值!
channel.basicPublish("rout.exchange", "email.key", null, (mess+"email").getBytes()); //emial.key
channel.basicPublish("rout.exchange", "sms.key", null, (mess+"sms").getBytes()); //sma.key
//关闭资源!
connection.close();
}
}
接收者
EmailkeyConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class EmailkeyConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("rout.exchange", BuiltinExchangeType.DIRECT);
//使用:自动生成的队列名;
String queueName = channel.queueDeclare().getQueue();
//读取数据时候,只要根据绑定 routing key 交换机就会往对应队列上发送数据;
channel.queueBind(queueName, "rout.exchange", "email.key");
//一个队列可以绑定多个Routingkey, 从而实现多种类型值...
// channel.queueBind(queueName, "rout.exchange", "sms.key"); //这里可以松解注释多次进行测试...
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
//使用默认 指定要监测的队列...
channel.basicConsume(queueName,true,defaultConsumer);
}
}
String queueName = channel.queueDeclare().getQueue();
获取到一个临时队列名称。
channel.queueDeclare():创建一个非持久化、独立、自动删除的队列名称
此队列是临时的,随机的,一旦我们断开消费者,队列会立即被删除
随机队列名,如amq.gen-jzty20brgko-hjmujj0wlg
测试
只有一个 email.key 没有松开注释的
松开注释的 email.key sms.key
nice, 这里我尽然搞了半小时才搞明白!踩了几个坑难受!
Routing模式和Publish/subscibe有啥区别?
-
Fanout 模式,一个发送者发送消息,所有的消费者都可以接收到消息!
而, 如果不同的消费者只是需要不同的数据,就会造成数据冗余,磁盘占用… -
Direct 模式 就可以实现,给不同的人不同的数据…
通过Routing key路由key
来实现!在交换机队列 绑定时候,指定 Routingkey可以设置多个
发送者发送数据, 根据 routingkey 指定发送消息的类型
接收者,通过 绑定交换机 + 队列 +routing key
来匹配, 确定消息发送到那个队列上!
官方场景:
- 程序运行时候,需要记录日至,error异常 info信息 warning警告⚠ …
- 而对于 error info waring 是需要运行时控制台展示;
- 而 error 是需要额外进行记录的,这时候就有两种消费者:
控制台
日志记录文件
它们所需要写入的数据是不一样的!,却来自一个发送者!!
注意:
-
Routing key 就相当于是,队列名一样
发送者 发送消息时候需指定 Routingkey 或 队列名
队列可以有多种别名Routingkey
表示消息的不同类型 -
在读取数据时候,
交换机 + 队列 + Routingkey 进行绑定
就可以根据队列名,来获取需要类型的数据集合了!
接收者监听方法,指定监听的队列 channel.basicConsume(queueName,true,defaultConsumer);
主题模式 Topics 交换机类型:TOPIC
该模式与Routingkey 非常类型,就相当于是一个 动态路由模式!!
-
每个消费者监听自己的队列
并且设置带统配符的routingkey,生产者将消息发给交换机,交换机根据Routingkey
来转发消息到指定的队列。 -
Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割例如:
sms.key email.key… -
而,主题模式 就可以根据一些特殊的符合匹配多种 Routingkey 的匹配
通配符规则:
#:匹配一个或多个词
举例:wsm.# 等于:wsm.1 / wsm.w.s.m / wsm.sm .后多个单词
*:匹配不多不少恰好1个词
举例:wsm.* 等于:wsm.sm / wsm.m .后一个单词
修改上面的Routingkey 即可:
发送者:
TopicsProducter.Java
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeoutException;
public class TopicsProducter {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//指定交换机 及 交换机的类型
channel.exchangeDeclare("tpc.exchange", BuiltinExchangeType.TOPIC);
//要发送的信息!
String mess = "一条消息:" + new SimpleDateFormat("yyyy-MM-dd hh:mm-ss").format(new Date());
channel.basicPublish("tpc.exchange", "wsm.email.key", null, (mess+"email").getBytes()); //emial.key
channel.basicPublish("tpc.exchange", "wsm.sms.key", null, (mess+"sms").getBytes()); //sma.key
//关闭资源!
connection.close();
}
}
消费者:
TopicsConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicsConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//连接
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("tpc.exchange", BuiltinExchangeType.TOPIC); //指定交换机类型TOPIC
//使用:自动生成的队列名;
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "tpc.exchange", "wsm.#"); //通配符Routingkey wsm.# 所有的key!
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body, "UTF-8");
System.out.println(str);
}
};
//使用默认 指定要监测的队列...
channel.basicConsume(queueName,true,defaultConsumer);
}
}
- 交换机类型
TOPIC
Routingkey 设置为:wsm.#
匹配所有 wsm.开头的Routing key… 因此 wsm.email.key wsm.sms.key 都将匹配!
测试:
扩展场景:
交换机head类型
headers类型的交换器的性能很差,不建议使用。
-
headers类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
-
消费者 再绑定队列和交换器时制定一组键值对
-
发送消息时候,提供:发送消息 和 要匹配的map
RabbitMQ会获取到该消息的headers,
对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对。 如果匹配,则该消息到此队列中 -
就相当于给队列设置了一个规范Map, 需要匹配才可以将,消息存入队列中去!
发送者:
HederProducter.Java
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.util.HashMap;
import java.util.Map;
//生产者
public class HederProducter {
public static void main(String[] args) throws Exception{
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("head.exchange", BuiltinExchangeType.HEADERS);
//发送的内容
String str ="header的内容lalala~~";
//发送的head map, 需要与队列的map 规范匹配才可以发送成功! 不然发送失败!
Map<String ,Object> param = new HashMap<String, Object>();
param.put("id","2");
param.put("name","wsm");
//设置Map 匹配参数!
AMQP.BasicProperties.Builder builder=new AMQP.BasicProperties.Builder();
builder.headers(param);
//发送参数 两次!
channel.basicPublish("head.exchange","",builder.build(),str.getBytes());
//关闭资源!
connection.close();
}
}
消费者:
HeaderConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
//消费者
public class HeaderConsumer {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare("head.exchange", BuiltinExchangeType.HEADERS); //交换机类型 heades
//设置队列上的 map 参数,用于匹配请求时候的参数!
//特殊参数 x-match 值 all 或 any
//all 在发布消息时携带的map 必须和绑定在队列上的所有map 完全匹配
//any 只要在发布消息时携带的有一对键值map 满足队列定义的多个参数map的其中一个就能匹配上
//注意: 这里是键值对的完全匹配,只匹配到键了,值却不一样是不行的;
Map<String ,Object> param = new HashMap<String, Object>();
param.put("x-match","all");
param.put("id","1");
param.put("name","wsm");
//声明队列 传入
channel.queueDeclare("headqueue", false, false, false, null);
//绑定
// 队列绑定时需要指定参数,注意虽然不需要路由键但仍旧不能写成null,需要写成空字符串""
channel.queueBind("headqueue", "head.exchange", "",param); //map参数,规范!
//----------------------------------------以上是声明 交换机/队列 绑定规范...
//以下是,消费者对消息的监听,获取!----------------------------------------....
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
Map<String, Object> headers = properties.getHeaders();
String str = new String(body, "UTF-8");
System.out.println("内容:"+str);
System.out.println("Map传入参数数据!"+headers);
}
};
channel.basicConsume("headqueue", true, defaultConsumer);
}
}
测试:
运行消费者 —— 生产者… 并尝试改变参查看效果..
- 上面Demo 生产者head map 不匹配消息发送失败!! 没有任何提示!
//消费者:队列规则all 所有匹配即可
Map<String ,Object> param = new HashMap<String, Object>();
param.put("x-match","all");
param.put("id","1");
param.put("name","wsm");
//生产者:传入head map;
Map<String ,Object> param = new HashMap<String, Object>();
param.put("id","2");
param.put("name","wsm");
//不匹配
//生产者
Map<String ,Object> param = new HashMap<String, Object>();
param.put("x-match","all");
param.put("id","1");
param.put("name","wsm");
//匹配
//消费者:队列规则any 一个匹配即可
Map<String ,Object> param = new HashMap<String, Object>();
param.put("x-match","any");
param.put("id","1");
param.put("name","wsm");
//生产者:传入head map;
Map<String ,Object> param = new HashMap<String, Object>();
param.put("id","1"); //匹配
//生产者:传入head map;
Map<String ,Object> param = new HashMap<String, Object>();
param.put("id","2"); //不匹配 key /value 都要匹配才可以
....
Confirm 消息确认
生产端 Confirm 消息确认机制
-
消息的确认,是指生产者投递消息后,如果 MQ 收到消息,则会给我们生产者一个应答。
-
生产者进行接收应答,用来确定这条消息是否正常的发送到 MQ,这种方式也是消息的可靠性投递的核心保障!
Confirm 确认机制流程图
如何实现Confirm确认消息?
- 第一步:在 channel 上开启确认模式:
channel.confirmSelect()
- 第二步:在 channel 上添加监听:
channel.addConfirmListener(ConfirmListener listener);
, 监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!
发送者:
ConfimProducer.Java
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.zb.util.ConnectionUtil;
import java.io.IOException;
public class ConfimProducer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//声明交换机 类型无所谓...
channel.exchangeDeclare("confim.exchage", BuiltinExchangeType.TOPIC);
//开启确认机制;
channel.confirmSelect();
//发送消息
channel.basicPublish("confim.exchage", "confim.key", null, "一条消息".getBytes());
//开启 confrim 消息认证机制...
channel.addConfirmListener(new ConfirmListener() {
/**发送成功!执行方法..
* long: 返回消息的,序列号
* boolean: 是否允许消息的批量发送!
*/
public void handleAck(long l, boolean b) throws IOException {
System.out.println(l);
System.out.println(b);
System.out.println("ack"); //已经发送ack确认
}
//发送失败!执行... 一般发送这种事情的处理方案...
public void handleNack(long l, boolean b) throws IOException {
System.out.println(l);
System.out.println(b);
System.out.println("nack"); //nack没有确认!
}
});
}
}
接收者:
ConfimConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
public class ConfimConsumer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//声明交换机
channel.exchangeDeclare("confim.exchage", BuiltinExchangeType.TOPIC);
//声明队列
channel.queueDeclare("confim.queue", true, false, false, null);
//绑定数据
channel.queueBind("confim.queue", "confim.exchage", "confim.#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
}
};
//监听消息, 传入回调函数...
//开启ack 自动认证...
channel.basicConsume("confim.queue", true, consumer);
}
}
测试:
- 先启动 接收者 —— 发送者;
没办法模拟成功!案例
Return消息机制
- Return Listener监听 用于处理一些不可路由的消息…
- 正常情况下消息生产者通过指定一个Exchange和RoutingKey, 把消息送到某一个队列中去, 然后消费者监听队列, 进行消费
- 但在某些情况下, 如果在发送消息的时候, 当前的exchange不存在或者指定的路由key路由不到?
这个时候如果我们需要监听这种不可达的消息, 就要使用Return Listener
- 在基础API中有一个关键的配置项Mandatory
- 如果为true, 则监听器会接收到路由不可达的消息, 然后进行后续处理
- 如果为false, 那么broker端自动删除该消息
发送者:
ReturnProducter.Java
import com.rabbitmq.client.*;
import com.zb.util.ConnectionUtil;
import java.io.IOException;
public class ReturnProducter {
public static void main(String[] args)throws Exception {
//连接!
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
String exchangeName = "return.exchage"; //设置交换机
String routingKey = "return.key"; //不存在的Routingkey: return1.key
//设置Return Listener监听.... 当然发送消息,交换机/队列错误..没有发送成功就会尽然这个方法!
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("交换机exchange:"+exchange);
System.out.println("交换机exchange数据:"+new String(bytes,"UTF-8"));
}
});
//发送一条数据,
channel.basicPublish(exchangeName, routingKey,true, null, "return一条消息".getBytes());
/**basicPublish 第三个参数...
* true: 时,交换器无法根据自动类型TOPIC 根据路由键找到一个符合条件的队列,那么RabbitMq会调用Basic.Ruturn命令将消息返回给生产者: Return Listener监听
* false:时,出现上述情况消息被直接丢弃
*/
}
}
接收者:
ReturnConsumer.Java
import com.rabbitmq.client.*;
import com.zb.util.ConnectionUtil;
import java.io.IOException;
public class ReturnConsumer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//声明: 交换机 队列 绑定!
channel.exchangeDeclare("return.exchage", BuiltinExchangeType.TOPIC);
channel.queueDeclare( "return.queue", true, false, false, null);
channel.queueBind( "return.queue", "return.exchage", "return.#");
//接收数据 回调!
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
}
};
channel.basicConsume( "return.queue", true, consumer);
}
}
测试:
运行 接收者 —— 发送者… 并手动更改 发送者的 Routingkey 值, 使数据发送不成功!
-
正常
-
更改Routingkey
-
设置false
channel.basicPublish(exchangeName, routingKey,false, null, "return一条消息".getBytes());
-
消息丢失没有结果…
RibbitMQ消费端限流
什么是消费端的限流
- 假设一个场景,首先,我们RabbitMQ服务器有上万条未处理的消息
- 我们随便打开一个消费者客户端,会出现下面情况:巨量的消息瞬间全部推送过来,
但是我们单个客户端无法同时处理这么多数据!
消费端限流RabbitMQ提供的解决方案
- RabbitMQ提供了一种qos(服务质量保证)功能,
- 即在非自动确认消息ack的前提下,
- 如果一定数目的消息(通过基于Consumer或者Channel设置Qos的值)未被确认前,不进行消费新的消息!
注意: 限流是限制的消费端的数据流动!
发送者:
QosProducter.Java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
public class QosProducter {
public static void main(String[] args) throws Exception {
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
String exchangeName = "qos.exchage";
String routingKey = "qos.key";
//循环发送五条数据!
for (int i = 0; i < 5; i++) {
channel.basicPublish(exchangeName, routingKey, true, null, ("qos一条消息" + i).getBytes());
}
}
}
消费者:
QosConsumer.Java
public class QosConsumer {
public static void main(String[] args) throws Exception {
Connection conn = ConnectionUtil.getConnection();
final Channel channel = conn.createChannel();
//创建 交换机 队列 并进行绑定!
channel.exchangeDeclare("qos.exchage", BuiltinExchangeType.TOPIC);
channel.queueDeclare("qos.queue", true, false, false, null);
channel.queueBind("qos.queue", "qos.exchage", "qos.#");
/**限流 Void BasicQos(uint prefetchSize, ushort prefetchCount, bool global);
*prefetchSize:0 不限制消息大小
*prefetchSize 会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该Consumer将block(阻塞)掉,直到有消息ack
*Global:true\false是否将上面设置应用于Channel;简单来说,就是上面限制是Channel级别的还是Consumer级别
*/
channel.basicQos(0, 3, false); //每次通过三条数据!
//发送消息,没有返回ack 的回调方法!
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("===========服务端发送的信息===============");
System.out.println(new String(body, "UTF-8"));
//代码手动回复ack,告诉服务端,以处理完当前的消息,你可以继续发下一条消息了!
//测试程序演示,注释掉改代码,不然它每一次都通过代码手动回复了...就看不到效果了...
// channel.basicAck(envelope.getDeliveryTag(),false); // false 处理批量数据
}
};
//发送数据,因为要查看限流的效果,默认的自动回复设置为 false 不自动回复!
// channel.basicConsume("qos.queue", true, consumer);
channel.basicConsume("qos.queue", false, consumer);
}
}
注意:
- prefetchSize和global这两项,RabbitMQ没有实现,暂且不研究;
prefetch_count在no_ask=false的情况下生效,即在自动应答的情况下,这两个值是不生效的
测试:
消息的ACK与重回队列
消费端手工ACK与NACK
- 消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿
一般都是记录下来,人工进行整改!
- 如果由于服务器宕机等严重问题,那么我们就需要手工进行ACK,保障消费端消费成功!
比如我们在消费端调用第三方接口,但这个第三方接口出现了问题导致接口调用失败,这时我们就要使消息重回队列
消费端的重回队列
- 消费端重回队列是为了对没有处理成功的消息,把消息重新回递给Broker
MQ服务
- 一般我们在实际应用中,都会
关闭重回队列
,也就是设置为False;因为重回队列消息有很大概率依然会处理失败
所以只要知道这个失败的消息,会重新回到队列就OK了…这是Rabbit MQ的一个特点吧…
发送者:
AckProducter.Java
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.util.HashMap;
import java.util.Map;
public class AckProducter {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//循环发送5条消息! 交换机 队列 都在消费者,那边创建好了!
for (int i = 0; i < 5; i++) {
Map<String, Object> header = new HashMap<String, Object>();
header.put("num",i);
//设置发送规格!虽然交换机不是hede 模式,不会进行规格验证!但照样而可以发送规格来,进行处理一些特殊方式!
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder()
.deliveryMode(2) //设置消息是否持久化,1:非持久化 2:持久化
.contentEncoding("UTF-8")
.headers(header).build();
channel.basicPublish( "ack.exchage" ,"ack.key",true, properties, ("ack一条消息" + i).getBytes());
/**basicPublish 第三个参数...
* true: 时,交换器无法根据自动类型TOPIC 根据路由键找到一个符合条件的队列,那么RabbitMq会调用Basic.Ruturn命令将消息返回给生产者: Return Listener监听
* false:时,出现上述情况消息被直接丢弃
*/
}
channel.close();
conn.close();
}
}
接收者:
AckConsumer.Java
import com.rabbitmq.client.*;
import com.zb.util.ConnectionUtil;
import java.io.IOException;
import java.util.Map;
public class AckConsumer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
final Channel channel = conn.createChannel();
//声明 交换机 队列 并进行绑定!
channel.exchangeDeclare("ack.exchage", BuiltinExchangeType.TOPIC); //交换机类型是 TOPIC
channel.queueDeclare("ack.queue", true, false, false, null);
channel.queueBind("ack.queue", "ack.exchage", "ack.#");
//对队列中消息处理的回调方法...
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
System.out.println("===========处理队列中消息!===========");
//为了方便看到效果,主线程休眠 2秒!
Thread.sleep(2000);
//获取到发送消息的 Map规格.... 因为交换机类型不是 HEADERS 所以Map规格不匹配照样发送成功!
Map<String, Object> headers = properties.getHeaders();
System.out.println(new String(body, "UTF-8"));
System.out.println("=========" + headers);
//模拟消息发送失败!头消息不为null 且 编号1的元素...
if (headers!=null && headers.get("num").toString().equals("1")) {
/**消息接收失败重回队列!不返回ack !
* void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
* deliveryTag :该消息的index
* multiple :是否批量. true:将一次性拒绝所有小于deliveryTag的消息。
* requeue :被拒绝的是否重新入队列, true将消息继续放到队列的末尾在此发送,false 不在放到队列中直接销毁!
* 本次设置true不拒绝,本次测试就是要重回队列,查看效果!
*/
channel.basicNack(envelope.getDeliveryTag(), false, true);
} else {
//消息接收成功不重回队列,返回 ack... 消息接收成功!
channel.basicAck(envelope.getDeliveryTag(), false);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("异常=================");
}
}
};
//监听队列中的消息..
channel.basicConsume("ack.queue", false, consumer);
}
}
- 以上Demo 需要细细品味~
测试:
TTL消息
- TTL是Time To Live的缩写,也就是
生存时间
- RabbitMQ支持消息的过期时间,在消息发送时可以进行指定, 消息的存在时间…
- RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息自动的清除 针对队列,只要是这个队列的消息,就只有这么长的存活时间
- 实现:
主要针对消息设置,跟交换机、队列、消费者设置毫无关系
发送者:
TTLProducter.Java
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
import java.util.HashMap;
import java.util.Map;
public class TTLProducter {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//通过channel发送消息
String msg = "hello rabbitmq!";
AMQP.BasicProperties properties = new AMQP.BasicProperties();
//设置规格!
Map<String,Object> headers = new HashMap<String, Object>();
headers.put("name", "wsm");
properties = properties.builder()
// 设置编码为UTF8
.contentEncoding("UTF-8")
// 设置自定义Header
.headers(headers)
// 设置消息失效时间
.expiration("5000").build();
//发送两条消息...
//设置了消息超时时间为5秒, 5秒后消息自动删除
channel.basicPublish("", "ttl_queue", properties, msg.getBytes());
//没有设置消息存活时间,消息存活时间根据队列来决定
channel.basicPublish("", "ttl_queue", null, msg.getBytes());
//关闭连接
channel.close();
conn.close();
}
}
接收者:
TTLConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TTLConsumer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//设置规格...
Map<String, Object> arguments = new HashMap<>();
// 设置队列超时时间为10秒
arguments.put("x-message-ttl", 10000); //固定的key属性规格!x-message-ttl 设置队列消息存储时间!
//声明(创建)一个队列... 这里没有绑定/指定交换机!
String queueName = "ttl_queue";
//绑定队列!
channel.queueDeclare(queueName,true, false, false, arguments);
//回调数据展示!
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取消息
System.out.println(new String(body, "UTF-8"));
//获取head
Map<String, Object> headers = properties.getHeaders();
System.out.println(headers);
}
};
//监听队列中的消息..
channel.basicConsume("ttl_queue", false, consumer);
}
}
测试:
- 先启动接收者:
创建出队列!
之后关闭即可!展示信息,不容易观看,直接在页面展示! - 启动提供者—— 查看 页面展示效果!
- 发送信息5秒内—— 5秒后
第一条消息过期
—— 10秒后队列中消息过期
死信队列
什么是 死信队列
-
消息变成死信有以下几种情况
消息被拒绝(basic.reject/basic.nack)并且requeue=false
消息TTL过期
队列达到最大长度 -
死信队列
利用DLX,当消息在一个队列中变成死信(dead message)之后,
它能被重新publish到另一个Exchange交换机上!
,这个Exchange就是DLX
死信队列的特点
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性;
- 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去
- 可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ3.0以前支持的immediate参数的功能;
生产者:
DlxProducter.Java
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wsm.util.ConnectionUtil;
public class DlxProducter {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
Channel channel = conn.createChannel();
//发送信息
//设置规格map
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder()
.deliveryMode(2) //设置消息是否持久化,1: 非持久化 2:持久化
.contentEncoding("UTF-8")
.expiration("5000") //设置消息过期时间... 死信产生时间!
.build();
//发送消息
channel.basicPublish( "dlx.exchage", "dlx.key", properties, ("一条消息!!").getBytes());
channel.close();
conn.close();
}
}
消费者:
DlxConsumer.Java
import com.rabbitmq.client.*;
import com.wsm.util.ConnectionUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DlxConsumer {
public static void main(String[] args) throws Exception {
//连接
Connection conn = ConnectionUtil.getConnection();
final Channel channel = conn.createChannel();
//设置一个普通交换机 队列 并绑定...
channel.exchangeDeclare("dlx.exchage", BuiltinExchangeType.TOPIC);
//设置绑定死信交换机参数... 如果: 交换机中出现死信,信息直接销毁并存放到死信交换机中! sx.exchange
Map<String, Object> agruments = new HashMap<String, Object>();
agruments.put("x-dead-letter-exchange", "sx.exchange");
//普通队列
channel.queueDeclare("dlx.queue", true, false, false, agruments);
//绑定
channel.queueBind("dlx.queue", "dlx.exchage", "dlx.#");
//定义死信交换机 TOPIC类型...
//Routingkey # 匹配所有的Routingkey 的值...
channel.exchangeDeclare("sx.exchange", BuiltinExchangeType.TOPIC);
channel.queueDeclare("sx.queue", true, false, false, null);
channel.queueBind("sx.queue", "sx.exchange", "#");
//监听到值后回调获取信息,进行处理!
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
}
};
//监听死信队列上的值...
channel.basicConsume("dlx.queue", true, consumer);
}
}
测试:
-
没啥好锁的,正常往交换机上添加数据, 数据out
死信
了,就换个交换机存储! -
死信交换机也只是一个普通的交换机,只是普通交换机设置了一些属性:
使当,产生死信时候进行处理的一个(死信)交换机
//死信交换机 TOPIC类型…
//Routingkey # 匹配所有的Routingkey 的值…
channel.exchangeDeclare(“sx.exchange”, BuiltinExchangeType.TOPIC);
channel.queueDeclare(“sx.queue”, true, false, false, null);
channel.queueBind(“sx.queue”, “sx.exchange”, “#”); -
运行接收者:
定义交换机...
关闭/开启都运行一次! -
接收者
正常工作
因为, 程序开启,未到过期时间… 消息不死正常输出!
-
接收者
不工作
5秒后死信队列上出现数据!
延时队列
- 在Rabbitmq中不存在
延时队列
- 但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。
延迟队列
消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
应用场景:
订单超时
- 相信大家都,买过快递 外卖, 确认购买开始付钱时候,2小时或者一段时间后不支付,订单就会失效!就是这个技术!
- 用户购买商品,产生订单;修改Radis 中库存…等操作…
- 订单存在一个 死信队列中,
并设置了过期时间
但并不会消费… - 一段时间过后,消息发送到死信队列中… 进行消费, 判断订单是否支付,未支付订单,将Radis库存加回来…!!
Boot 整合 RabbitMQ :
终于要写完了!!
重新开启一个项目工程!
搭建SpringBoot环境
添加AMQP的启动器:依赖
:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父依赖! -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.zb</groupId>
<artifactId>bootMQ</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- amqp依赖! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- web依赖! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
.yml 配置:
application.yml
spring:
rabbitmq:
host: 127.0.0.1 #ip
port: 5672 #端口
username: guest #用户
password: guest #密码
virtual-host: / #虚拟路径!
#有关AmqpTemplate的配置
# template:
# retry:
# enabled: true
# initial-interval: 10000ms
# max-interval: 300000ms
# multiplier: 2
# exchange: topic.exchange
# publisher-confirms: true
template:有关AmqpTemplate的配置
- retry:失败重试
- enabled:开启失败重试
- initial-interval:第一次重试的间隔时长
- max-interval:最长重试间隔,超过这个间隔将不再重试
- multiplier:下次重试间隔的倍数,此处是2即下次重试间隔是上次的2倍
- exchange:缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个
- publisher-confirms:生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试
当然如果consumer只是接收消息而不发送,就不用配置template相关内容。
配置类:
MQConfig.Java
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //Boot配置类注解;
public class MQConfig {
//队列
public static final String QUEUE_EMAIL = "boot.email";
//交换机
public static final String EXCHANGE_TOPIC_INFORM = "exchange.topic.boot";
//创建交换机,并交给Spring管理
@Bean(EXCHANGE_TOPIC_INFORM)
public Exchange createExchange() {
//durable(true) 持久化,mq重启之后交换机还在
return ExchangeBuilder.topicExchange(EXCHANGE_TOPIC_INFORM).durable(true).build();
}
//创建队列交给Spring管理
/*
* new Queue(QUEUE_EMAIL,true,false,false)
* durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
* auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
* exclusive 表示该消息队列是否只在当前connection生效,默认是false
*/
@Bean(QUEUE_EMAIL)
public Queue createEmailQueue() {
Queue queue = new Queue(QUEUE_EMAIL);
return queue;
}
/**绑定交换机/队列 并指定Rontingkey!
*
* @param exchange 指定交换机 @Qualifier注解根据参数名进行匹配Spring映射!
* @param queue 指定队列!
* @return
*/
@Bean
public Binding createEmailToExchange(@Qualifier(EXCHANGE_TOPIC_INFORM) Exchange exchange, @Qualifier(QUEUE_EMAIL) Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("info.#").noargs();
}
//如果后面还需要进行什么配置...在该配置类中继续扩展...
}
发送者:
SendService.Java
import com.zb.config.MQConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
//消息生产者,业务层...
public class SendService {
@Autowired
private RabbitTemplate rabbitTemplate; //amqp依赖中提供的 rabbit类!
//发送一条消息!
public void send() {
String msg = "一条boot的消息";
Map<String ,Object> param = new HashMap<>();
param.put("msg",msg);
/**
* 指定发送交换机!
* Routingkey
* 发送的数据..Object类型,发送时会进行序列化转换! 这边传什么类型——接收者 接收就以该类型接收即可!
*/
rabbitTemplate.convertAndSend(MQConfig.EXCHANGE_TOPIC_INFORM, "info.email", param);
}
}
消费者:
MQListener.Java
import com.zb.config.MQConfig;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component //类上的注解,注册到Spring容器
//MQ监听类: 用来获取数据
public class MQListener {
/**
* @RabbitListener(queues = MQConfig.QUEUE_EMAIL) 注解: queues 要监听的队列...
*
* @param param 监听消息结果返回的类型!
* 监听操作是实时的,只要队列中有数据,这个方法就会执行!
*/
@RabbitListener(queues = MQConfig.QUEUE_EMAIL)
public void revice(Map<String, Object> param) {
System.out.println(param);
}
}
监听属性!
主程序 测试运行:
MyBootApp.Java
import com.zb.service.SendService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class MyBootApp {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MyBootApp.class, args);
//主程序运行,并获取消息生产者... 执行生产数据!
SendService bean = run.getBean(SendService.class);
bean.send();
}
}
唉~ 写了一下午了,要个赞不过分吧…