1 相关概念
1.1什么是MQ(message queue)
本质是个队列,FIFO先入先出,存放的内容是message,而且还是一种跨进程的通信机制,用于传递上下游消息,在互联网架构中MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务,使用MQ之后,消息发送上游只需要依赖MQ不需要依赖其他服务。
1.2.为什么用MQ
流量削峰
如果系统最多可以处理10000并发量,平时使用肯定时绰绰有余,1秒就可以反馈结果,但是如果是高峰期可能同时出现20000或者更多的并发量,超过10000的我们只能禁止他进行该操作。使用MQ做缓冲之后我们可以把每秒的操作分配到一段时间内来处理,这样会造成操作之后几十秒以后才会收到结果反馈,但是比限制操作要有好很多。也可以防止后端系统宕机。但是相应的访问速度会下降。
应用解耦
一个应用中有多个系统,如果A系统宕机,那么与其耦合调用的B系统也会发生异常,最终造成整个应用的宕机,而使用了MQ之后,如果A系统鼓掌,B系统的请求被缓存在消息队列种,当A系统恢复后,再从消息队列种拿到请求进行处理,这样可以避免很多因系统调用造成的问题。
异步处理
当A系统调用了B系统处理一个很长时间才可以完成的请求,并且A需要知道B是否处理完成,以前一般有两种方式:第一种是A过一段时间去调用一下B提供的查询API来确认,第二种是B提供一个calllback的api当B处理完成之后调用一下A的callback,这两种方式不是很优雅。但是使用消息总线可以解决这个问题,当A调用B后,去监听B的处理完成的消息,B处理完成后,会向MQ发送一个消息,由MQ转发给A,当A监听到后就代表B已经处理完成,这样既避免了A重复调用B的查询接口,也不用提供callback的API,同样B也不用做这些操作,A还可以及时的到B的处理反馈。
1.3.MQ的分类
ActiveMQ
比较老的MQ
优点:
单机吞吐量可达万级,时效性是ms级,可用性高,基于主从框架实现高可用性,消息可靠性,较低的概率丢失数据
缺点:
官方对于ActiveMQ5.x的维护较少,高吞吐量使用场景较少
Kafka
使用率较高,大数据工程师的杀手锏,一般大数据内的消息传输绕不开Kafka,这款为大数据而生的消息中间件,以其百万级TPS吞吐量而闻名,在数据采集,传输,存储中发挥着重要的作用。
优点:
性能卓越,单机写入TPS在百万条/秒,最大的有点就是吞吐量高,时效性ms,可用性非常高,Kafka是分布式的,一个数据多个副本,少数宕机不会丢失数据,不会导致不可用,消费者采用Pull方式获取消息,消息有序,通过控制能够保证消息仅被消费者使用一次;有优秀的Kafka WEB管理页面Kafka-Manager;在日志领域比较成熟,功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算和日志采集大规模使用。
缺点:
单机超过64个队列/分区,Load会发生明显的飚高现象(CPU飚高),队列越多,load越高,消息响应时间就会变长,使用短轮询方式,时效性取决于短轮询间隔时间,消费失败不支持重试,支持消息顺序,但是一台代理宕机后会出现消息乱序,社区更新较慢。
RockerMQ
出自阿里巴巴的开源产品,参考了Kafka,并做出了一些改进,被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志,binlog分发等场景。
优点:
单机吞吐量10万级,分布式架构,可用性很高,可以做做到消息0丢失,MQ功能较为完善,扩展性好,支持10亿级别的消息堆积,不会因为消息堆积而造成性能下降,源码是java可以自己阅读并定制自己公司的MQ。
缺点:
支持客户端较少,目前主要是java,c++,c++目前不太成熟。没有在MQ核心去实现JMS等接口,有些系统需要迁移大量代码。
RabbitMQ:
2007年发布,是一个在AMQP(高级消息队列协议)的基础上完成的,可复用的企业消息队列系统,是当前最主流的消息中间件之一。
优点:
由于erlan语言的高并发特性,性能较好,吞吐量万级,MQ功能比较完备,健壮,稳定,医用,跨平台,支持多种语言(Python,ruby,.net,JAVA,JMS,C,PHP,ActionScript,XMPP,STMP等)支持AJAX文档齐全,开源提供管理界面非常棒,用起来很好用,社区活跃度高,更新频率高
缺点:商业版需要收费,学习成本较高。
官网:www.rabbitmq.com/news.html
1.4.MQ选择
1.Kafka:适合产生大量数据的互联网服务的数据收集业务,大型公司可以选用,如果有日志采集功能首选Kafka。
2.RocketMQ:为金融互联网而生,对于可靠性要求很高的场景,以及业务削峰,如果类似于阿里的双十一这样的高并发场景,建议选用。
3.RabbitMQ:结合elang语言本身特点,性能好时效性微秒级,社区活跃度很高,管理界面用起来十分方便,如果数据量没有那么大,中小型公司有效选的RabbitMQ。
2.RabbitMQ
2.1.RabbitMQ的概念
RabbitMQ是一个消息中间件;他接受并转发消息,可以把他当做一个快递站点,当你发送一个包裹时,你把你的包裹放到快递点,快递员会把你的快递送到收件人那里,他和快递站最大的区别在于,他不处理快件,而是接收,存储,转发消息数据。
2.2.四大核心概念
生产者:产生数据发送消息的程序是生产者
交换机:他一方面接受来自生产者的消息,另一方面将消息推送到队列中,交换机必须确切的知道如何处理它接收道德消息,试讲这些消息推送到特定的队列还是多个队列,亦或者丢弃,这都由交换机类型决定。(一个交换机可以对应一个队列也可以对应多个队列)
队列:他只能将消息储存在队列中,仅受到主机和磁盘限制的约束,本质上是一个消息称缓存区,许多生产者可以将消息发送到一个队列,许多消费者也可以从一个队列接收数据。
消费者:消费与接收的含义相似,大多数时候消费者是一个等待接收消息的程序,请注意生产者,消费者和消息中间件很多时候不在同一个机器上,同一个应用程序既可以是生产者也可以是消费者。
2.3.六大核心部分(六大模式)
2.3.1 Hello Word(简单模式)
2.3.2 Work Queues(工作模式)
2.3.3 Publish/Subscribe(发布订阅模式)
2.3.4 Routing(路由模式)
2.3.5 Topics (主题模式)
2.3.6 Publicher Confirms(发布确认模式)
2.4.RabbitMQ名词关系
Producer:生产者
Broker:接收和转发消息,RabbitMQ Server 就是Message Broker,包含交换机和队列
Connection:连接,每一个生产者,消费者和Broker会建立一个链接。而每一个连接中会有多个信道(Channel),创建链接消耗比较大,所以只建立一次链接,但是有多个信道,每次发消息只占用一个信道。
2.5.安装
MQ安装步骤
官网:www.rabbitmq.com/download.html
1.升级所有包同时也升级软件和系统内核
yum -y update
-
yum -y update 升级所有包同时也升级软件和系统内核yum -y upgrade
-
只升级所有包,不升级软件和系统内核
2.安装EPEL YUM源
yum -y install epel-release
3. 安装Erlang 环境
yum -y install erlang socat
4.查看版本
erl -version
5. 我们这时候就需要去看看官网上 Erlang 对应版本的 RabbitMQ 需要我们下载什么版本。
6.根据系统版本Elang环境版本下班相应的MQ,然后去安装一下
7.下载成功之后上传到Linux中运行
rpm -Uvh rabbitmq-server-3.7.26-1.el8.noarch.rpm
8.启动MQ
systemctl start rabbitmq-server
9.查看状态
systemctl status rabbitmq-server
其他命令
设置开机自启动
chkconfig rabbitmq-server on
启动命令
/sbin/service rabbitmq-server start
查看服务状态
/sbin/service rabbitmq-server status
停止服务
chkconfig rabbitmq-server stop
MQ后台管理安装
1.关闭MQ进程,查看状态
2.安装插件
rabbitmq-plugins enable rabbitmq_management
3.重新运行
chkconfig rabbitmq-server start
4.访问域名+15672,发现并不能访问,因为防火墙未关闭,关闭防火墙
systemctl stop firewalld
开机不启动防火墙
systemctl enable firewalld
开放端口
sudo firewall-cmd --add-port=15672/tcp --permanent
重启防火墙
firewall-cmd --reload
查看开放端口号
firewall-cmd --list-all
5.访问地址
默认账号密码为:guest guest
提示没有权限,不能登陆
6.添加一个新的账户
rabbitmqctl add_user admin 123
7.设置账户权限
超级管理员
rabbitmqctl set_user_tags admin adminsrator
设置读写权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
8.查看全部用户权限
rabbitmqctl list_users
3.JAVA集成
3.1.Hello Word (简单模式)
3.1.1.创建maven项目,并引入依赖
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
3.1.2.创建生产者
package com.rabbitmq.one;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
//队列名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂
ConnectionFactory factory=new ConnectionFactory();
//设置工厂ip 连接队列
factory.setHost("112.124.34.53");
//设置用户名,密码
factory.setUsername("admin");
factory.setPassword("123");
//创建链接
Connection connection = factory.newConnection();
//获取连接中的信道
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 参数:
* 1.队列名称
* 2.是否持久化(默认在内存中,是否持久化到磁盘上)
* 3.是否消息共享(该队列是否只供一个消费者消费)
* 4.是否自动删除(该队列断开连接以后,是否自动删除)
* 5.其它参数(例如:延时消息,死信消息)
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String massage="hello word";
/**
* 发送消息
* 参数:
* 1.发送到哪个交换机
* 2.路由的key(本次为队列名)
* 3.其它参数
* 4.发送的消息
*/
channel.basicPublish("",QUEUE_NAME,null, massage.getBytes());
System.out.println("消息已发送完毕");
}
}
有三个准备完毕的消息。
3.1.3.创建消费者
package com.rabbitmq.one;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Description
* 消费者接受消息
* @ClassName Consumer
* @Author LY
* @Date 2023/10/30 10:31
**/
public class Consumer {
//队列名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂
ConnectionFactory factory=new ConnectionFactory();
//设置工厂ip 连接队列
factory.setHost("112.124.34.53");
//设置用户名,密码
factory.setUsername("admin");
factory.setPassword("123");
//创建链接
Connection connection = factory.newConnection();
//获取连接中的信道
Channel channel = connection.createChannel();
/**
* 消费消息
* 参数:
* 1.消费哪个队列
* 2.消费成功是否自动应答
* 3.消费者未成功消费的回调(接口)
* 4.消费者取消消费的回调(接口)
*/
//声明参数3 接收消息
DeliverCallback deliverCallback=(consumerTag,massage)->{
System.out.println(new String(massage.getBody()));
};
//声明参数4 取消消息接收时的回调
CancelCallback cancelCallback=consumerTag->{
System.out.println("消息接收被中断");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
3.2.Work Queues(工作模式)
该模式主要为了避免立即执行资源密集型任务,而不得不等待他的完成。相反我们安排任务在之后执行,我们把任务作为消息发送到队列内,在后台运行的工作进程将弹出任务并最终执行作业,当有多个线程时,这些工作线程将一起处理这些任务。
3.21.轮训分发
一个生产者发送消息,多个工作线程接收,工作线程为竞争关系且一个消息只会被处理一次。
3.2.2.抽取工具类
package com.rabbitmq.config;
public class RabbitMQConfig {
public static final String HOST="112.124.34.53";
public static final String USERNAME="admin";
public static final String PASSWORD="123";
}
package com.rabbitmq.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Description
* 获取连接工具类
* @ClassName RabbitMQUtil
* @Author LY
* @Date 2023/10/30 15:49
**/
public class RabbitMQUtil {
//获得一个链接
public static Channel getChannel(String host, String userName, String passWord) throws IOException, TimeoutException {
//新建一个工厂
ConnectionFactory factory=new ConnectionFactory();
//设置参数
factory.setHost(host);
factory.setUsername(userName);
factory.setPassword(passWord);
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
return channel;
}
}
3.2.3.消费者代码
package com.rabbitmq.two;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Worker {
//队列名称
public static final String QUEUE_NAME="hello";
}
class Worker01{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("Worker01等到接受消息");
//接收消息
channel.basicConsume(Worker.QUEUE_NAME,true,new MyDeliverCallback(),new MyCancelCallback());
}
}
//消费者接收消息参数3
class MyDeliverCallback implements DeliverCallback {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("接收到的消息:" + new String(delivery.getBody()));
}
}
//消费者接收消息参数4
class MyCancelCallback implements CancelCallback {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收被中断");
}
}
3.2.4.生产者代码
package com.rabbitmq.two;
import com.rabbitmq.client.Channel;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
for (int i = 1; i <= 20; i++) {
String massage="hello word"+i;
channel.basicPublish("",QUEUE_NAME,null, massage.getBytes());
System.out.println("消息已发送完毕"+massage);
}
}
}
3.2.5.结果:
消息会轮流发送给work01,work02,work01,work02...
3.3.消息应答
3.3.1.概念:
如果有一个逻辑比较复杂,处理时间比较长,其中某个线程工作只进行了一部分然后宕机了,导致任务并没有完成,然后队列中的消息又被删除了,那么就意味着消息丢失了。
为了防止消息丢失,RabbitMQ引入了消息应答,消费者在接收并处理消息完成之后,消费者会通知MQ,此时MQ才会删除消息。
自动应答:
这种模式需要在高吞吐量和数据传输安全性方面做权衡,没有对消息的数量进行限制,所以这种模式仅是用于消费者可以高效并以某种速率能够处理这些消息的情况下使用。
手动应答:
A.Channel.basicAck(用于肯定应答):
表示MQ已经知道该消息并且成功的处理消息,可以将其丢弃了。
B.Channel.basicNack(用于否定确认)
C.Channel.basicReject(用于否定确认):与Channel.basicNack相比少一个参数(是否批量处理),表示不处理该消息了,直接拒绝,可以将其丢弃。
3.3.2.Multiple的解释
手动应答的好处是可以批量处理,减少网络拥堵。
例如:Channel.basicAck(8,multiple),如果multiple为true的话他会处理此信道上所有的未被确认的应答消息,而如果是false则只处理8。
3.3.3.消息自动重新入队
如果由于某些原因失去连接(其通道已关闭,链接已关闭或TCP链接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将其重新排队,如果此时其他消费者可以处理,他将会很快的将其重新分发给另一个消费者,这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
3.3.4.消息手动应答代码
package com.rabbitmq.three;
import com.rabbitmq.backInterface.MyCancelCallback;
import com.rabbitmq.backInterface.MyDeliverCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class Main {
}
/**
* @Description
* 生产者发送消息
* @ClassName Producer
* @Author LY
* @Date 2023/10/31 14:00
**/
class Producer{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个队列
channel.queueDeclare(RabbitMQConfig.TASK_ACK_QUEUE_NAME,false,false,false,null);
//输入信息
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
String msg = scanner.next();
channel.basicPublish("",RabbitMQConfig.TASK_ACK_QUEUE_NAME,null,msg.getBytes("UTF-8"));
System.out.println("生产者发出消息:"+msg);
}
}
}
/**
* @Description
* 消费者缓慢处理消息(10秒)
* @ClassName Consumer01
* @Author LY
* @Date 2023/10/31 14:00
**/
class Consumer01{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("Consumer01接收消息,沉睡50秒");
DeliverCallback deliverCallback=(s,delivery) ->{
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("接收到的消息:" + new String(delivery.getBody()));
/**
* 手动应答
* 参数:
* 1.消息标记 tag delivery.getEnvelope().getDeliveryTag()
* 2.是否批量应答
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
Boolean autoAsk=false;
//采用手动应答
channel.basicConsume(RabbitMQConfig.TASK_ACK_QUEUE_NAME,autoAsk,deliverCallback,new MyCancelCallback());
}
}
/**
* @Description
* 消费者快速处理消息
* @ClassName Consumer01
* @Author LY
* @Date 2023/10/31 14:00
**/
class Consumer02{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("Consumer02接收消息");
Boolean autoAsk=false;
//采用手动应答
channel.basicConsume(RabbitMQConfig.TASK_ACK_QUEUE_NAME,autoAsk,new MyDeliverCallback(),new MyCancelCallback());
}
}
使用一个Thread.sleep(50000)来模拟一个复杂的业务逻辑,当我们对生产者输入消息AA时,第一个Consumer01会在50秒后输出小心并应答,此时该消息会被MQ销毁,Consumer02模拟一个简单的业务逻辑,当我们发送第二个消息BB会被Consumer02瞬间处理,此时我们发送CC,当消息发送成功后,我们关闭Consumer01,此时模拟系统意外宕机等突发情况,而Consumer01关闭后,Consumer02则会处理我们发送的消息CC被Consumer01接收到后,处理消息结束之前并未被销毁,这样可以有效防止各种突发状况导致的数据丢失。
3.4.RabbitMQ持久化
3.4.1.概念
当某个系统意外关闭或宕机时可以使用手动应答来防止数据丢失,而如果有意外情况导致RabbitMQ意外关闭或,他会忽视队列和消息,除非告知他不要这样做,确保消息不会丢失需要做两件事:队列持久化,消息持久化。
3.4.2.队列持久化
之前我们创建的队列都是非持久化的,MQ如果重启,该队列就会被删除掉,要实现队列持久化,需要在生命队列时把durable参数设置为持久化。
但是需要注意之前的队列并非持久化的,需要把原先的队列删除掉,或者重新创建一个持久换的队列,否则就会出现错误。
非持久化队列:
持久化队列:
3.4.3.消息持久化
要想让消息持久化需要再生产者发送消息时,设置第三个参数为:MessageProperties.PERSISTENT_TEXT_PLAIN
此参数为标记消息持久化,并且不能完全保证消息不会丢失,尽管他会标记消息保存到磁盘,但是他在消息写入磁盘之前,存在一个间隔点。此时并没有写入磁盘,持久性并不欠,但是对于简单队列而言已经绰绰有余了。如果需要更强有力的持久化策略,则需要配合发布确认模式使用。
3.4.4.不公平分发
MQ默认情况下才从轮训分发,但是在某种业务场景下,这种策略并非很好,比如两个消费者在处理任务,一个非常快,一个非常慢,这个时候继续采用轮训分发就会导致快的大部分时间都处于空闲状态,而慢的一直都在处理任务,这种情况下轮训分发其实并不太好,但是MQ并不知道会出现这种情况,在我们不指定的情况下,他依旧轮讯分发。
为了避免这种情况,我们可以设置参数channel.basicQos(1)
这个设置应该由消费者在接收消息之前来设置。
3.4.5.预期值
channel.basicQos(prefetch),这个参数类似于权重(ngnix的weight参数),我们设置消费者A为5,消费者B为3,那么无论他们谁处理的快,谁处理的慢,8个消息一定会分发给消费者A5条,消费者B 3 条。
3.5.发布确认模式
1.必须设置队列必须持久化。
2.必须设置队列中的消息持久化。
3.发布确认。
生产者发送消息之后,MQ将信息保存到磁盘上之后,MQ会向生产者发送一个信息,消息已经被保存在磁盘上,这样可以完全保证,消息已经被MQ保存在磁盘上,消息完全不会丢失。
3.5.1.开启发布确认方法
再生产者中,发消息之前,创建信道之后,调用channel.confirmSelect();开启发布确认。
Channel channel = RabbitMQUtil.getChannel();
channel.confirmSelect();
3.5.2.单个发布确认
这是一种简单的确认方式,它是一种同步确认发布的方式,只有前一个消息确认发布后,才会继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间内没有被确认,那么他将抛出异常。
这个方式有一个最大的缺点,发布速度特别慢,因为如果没有确认发布,就会阻塞所有的后续消息发布,这种方式最多提供每秒不超数百条的消息吞吐量,当然对于某些应用程序来讲,这已经足够了。
//单个确认
public static void confirmOne() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认
channel.confirmSelect();
long star=System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String s=i+"";
channel.basicPublish("",queueName,null,s.getBytes());
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功"+i);
}
}
long end=System.currentTimeMillis();
long l = end - star;
System.out.println("发布"+MESSAGE_COUNT+"条单个确认耗时"+l);
//1.单个发布确认 发布1000条单个确认耗时38759ms
}
3.5.3.批量发布确认
与单个确认相比,先发布一批消息,然后以确认可以极大地提高吞吐量,当然这种方式的缺点就是,党发不出现问题是,不知道哪个消息出现了问题,我们必须将整个批处理保存在内存中,用以记录重要的信息二手重新发布,当然这种方式也是同步的,也一样阻塞线程。
//批量确认
public static void confirmBatch() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
long star = System.currentTimeMillis();
//批量发布确认
//批量确认消息条数
Integer bathCount = 100;
for (int i = 1; i <= MESSAGE_COUNT; i++) {
String s = i + "";
channel.basicPublish("", queueName, null, s.getBytes());
if (i % bathCount == 0) {
//每100条确认一次
channel.waitForConfirms();
System.out.println("消息发送成功" + i);
}
}
long end = System.currentTimeMillis();
long l = end - star;
System.out.println("发布" + MESSAGE_COUNT + "条每100条确认耗时" + l);
//发布1000条每100条确认耗时390
}
3.5.4.异步发布确认
相比于前两个同步确认,他的可靠性高,效率好,他利用回调函数来达到让消息的可靠传递的,这个中间件也是通过函数回调来确认消息是否投递成功。成功:确认应答,未成功:未确认应答。成功的不做处理,未收到的重新发送。因为是异步处理,所以生产者只需要发送数据,等待应答即可。由于是异步确认,所以会先发布成功,后续还会确认。
//异步确认
public static void asynConfirm() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
long star = System.currentTimeMillis();
//异步发布确认
//准备监听器,监听发送成功和失败
channel.addConfirmListener(new MyConfirmCallbackSuccess(), new MyConfirmCallbackFail());
//批量确认消息条数
for (int i = 1; i <= MESSAGE_COUNT; i++) {
String s = i + "";
channel.basicPublish("", queueName, null, s.getBytes());
}
long end = System.currentTimeMillis();
long l = end - star;
System.out.println("发布" + MESSAGE_COUNT + "条异步确认耗时" + l);
//发布1000条异步确认耗时81
}
成功回调函数
package com.rabbitmq.backInterface;
import com.rabbitmq.client.ConfirmCallback;
import java.io.IOException;
//成功回调函数
public class MyConfirmCallbackSuccess implements ConfirmCallback {
/**
* @Description
* @Author LY
* @Param [l, b]
* l:消息的标识,b是否批量确认
* @return void
* @Date 2023/11/1 10:09
**/
@Override
public void handle(long l, boolean b) throws IOException {
System.out.println("确认的消息:"+l);
}
}
失败回调函数
package com.rabbitmq.backInterface;
import com.rabbitmq.client.ConfirmCallback;
import java.io.IOException;
//失败回调函数
public class MyConfirmCallbackFail implements ConfirmCallback {
/**
* @Description
* @Author LY
* @Param [l, b]
* l:消息的标识,b是否批量确认
* @return void
* @Date 2023/11/1 10:09
**/
@Override
public void handle(long l, boolean b) throws IOException {
System.out.println("未确认的消息:"+l);
}
}
3.5.5.异步发布未确认消息
对于异步发布确认,消息发布未成功的消息应该有后续操作,无论是存储,还是重新发送,都要有一个处理方式。
最好的解决方案是吧未确认的消息放到一个基于内存的能被发布线程访问到的队列,比如说用ConcurrentLinkedQueue这个对俩在confirm callbacks与发布线程之间进行消息的传递。
这里需要三步:
1.消息发送之后记录所有已发送的消息
2.确认成功之后删除掉已确认的 消息
3.对未确认的消息进行特殊处理
//异步确认
public static void asynConfirm() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
/**
* 线程安全有序地hashtable,是用于高并发的情况
* 1.轻松地将序号与消息进行关联
* 2.轻松批量的删除条数
* 3.支持高并发多线程
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms=new ConcurrentSkipListMap<>();
long star = System.currentTimeMillis();
//异步发布确认
//准备监听器,监听发送成功和失败
channel.addConfirmListener(new MyConfirmCallbackSuccess(outstandingConfirms), new MyConfirmCallbackFail(outstandingConfirms));
//批量确认消息条数
for (int i = 1; i <= MESSAGE_COUNT; i++) {
String s = i + "message";
channel.basicPublish("", queueName, null, s.getBytes());
//1.记录下所有发送的消息
outstandingConfirms.put(channel.getNextPublishSeqNo(),s );
}
long end = System.currentTimeMillis();
long l = end - star;
System.out.println("发布" + MESSAGE_COUNT + "条异步确认耗时" + l);
//发布1000条异步确认耗时81
}
成功处理:
package com.rabbitmq.backInterface;
import com.rabbitmq.client.ConfirmCallback;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Locale;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.io.IOException;
//成功回调函数
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MyConfirmCallbackSuccess implements ConfirmCallback {
private ConcurrentSkipListMap concurrentSkipListMap;
/**
* @return void
* @Description
* @Author LY
* @Param [l, b]
* l:消息的标识,b是否批量确认
* @Date 2023/11/1 10:09
**/
@Override
public void handle(long l, boolean b) throws IOException {
if (b) {
//如果是批量 清空
ConcurrentNavigableMap<Long, String> confirmed = concurrentSkipListMap.headMap(l);
confirmed.clear();
} else {
//非批量删除单个
concurrentSkipListMap.remove(l);
}
//2.删除确认的消息 剩余的是未确认的消息
System.out.println("确认的消息:" + concurrentSkipListMap.get(l)+" 消息标记:"+l);
}
}
未确认处理:
package com.rabbitmq.backInterface;
import com.rabbitmq.client.ConfirmCallback;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.util.concurrent.ConcurrentSkipListMap;
//失败回调函数
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MyConfirmCallbackFail implements ConfirmCallback {
private ConcurrentSkipListMap concurrentSkipListMap;
/**
* @Description
* @Author LY
* @Param [l, b]
* l:消息的标识,b是否批量确认
* @return void
* @Date 2023/11/1 10:09
**/
@Override
public void handle(long l, boolean b) throws IOException {
Object msg = concurrentSkipListMap.get(l);
System.out.println("未确认的消息:"+msg+"未确认的消息标记是:"+l);
}
}
3.5.6.三种模式速度对比
//1.单个发布确认
confirmOne();//1.单个发布确认 发布1000条单个确认耗时38759ms
//2.批量发布确认
confirmBatch();//发布1000条每100条确认耗时390
//3.异步发布确认
asynConfirm();//发布1000条异步确认耗时81
单独发布消息:同步等待确认,简单,但是吞吐量有限
批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题很难具体到哪条消息。
异步处理:最佳的性能和资源使用,再出现错的情况下可以很好的控制,但实现起来稍难。
4交换机
在之前的模式中,我们创建了一个工作队列,假设工作队列的背后,每个人物都签好交付给一个消费者(工作进程)。在这一部分中,将做一些完全不同的事情,将消息传达给多个消费者,这种模式我们称之为:“发布/订阅模式”。
例如一个简单的系统日志。他由两个程序组成,第一个程序将发出日志消息,第二个程序是消费者,其中消费者将会启动两个,一个接收到后将消息存储在磁盘上,第二个消费者把消息打印在屏幕上,事实上第一个程序发出的等消息日志将官拨给所有消费者。
之前的模式,消费者都是竞争关系,同一个队里中的同一份消息只会被消费一次。
4.1.Exchanges
4.1.1.Exchanges概念
RabbitMQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列,就算是简单模式,我们也走的是默认交换机。实际上,通常生产者甚至都不知道这些消息传递带了那些队列中。想法生产者只能将消息发送到交换机(exchange),交换机的工作内容非常简单,一方面他接受来自生产者的消息,另一方面将他们推入队列。交换机必须确切知道如何处理收到的消息,是应该把这些消息放到特定的队列还是把他们放到许多队列中,或者应该丢弃他们,这就由交换机的类型来决定。
4.1.2.Exchange类型
直接(direct)(路由类型)
主题(topic)
标题(headers)(头类型)
扇出(fanout)(发布订阅类型)
无名exchange
4.1.3.无名exchange
之前的发送消息我们并没有制定交换机,之前之所以能实现将消息发送到队列中,因为我们使用的是默认交换机,通常用字符串(“”)进行标识。
channel.basicPublish("", queueName, null, s.getBytes());
第一个参数是交换机名称,空字符串表示默认或者无名交换机;消息之所以能发送到队列中其实是由routingKey()绑定key指定的。
4.2.临时队列
每当我们连接到RabbitMQ时,我们需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者让服务器为我们选择一个随机队列名称那就更好了,其次我们一旦断开连接,队列就会自动删除,队列没有被持久化,持久化标记没有。
String queueName=channel.queueDedare().getQueue;
4.3.绑定(bindings)
1.Add a new queue
2.添加一个交换机
3.交换机与队列绑定
4.通过123与hello1相连接
4.4.fanout(发布订阅)(扇出)
4.4.1.概念
这种类型非常简单,正如名称中那样,它是将受到的所有信息,广播到他知道的所有队列中,系统中默认有些exchange类型。
4.4.2.fanout代码
package com.rabbitmq.five;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class Logs {
}
class EmitLogs{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
Scanner sc=new Scanner(System.in);
while (sc.hasNext()){
String msg=sc.next();
channel.basicPublish(RabbitMQConfig.EXCHANGE_NAME,"",null,msg.getBytes());
System.out.println("生产者发出消息:"+msg);
}
}
}
class ReceiveLogs01{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
//声明一个队列 临时队列 队列名称随机
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"");
System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
channel.basicConsume(queue, true, (s,delivery) ->{
System.out.println("ReceiveLogs01控制台打印:"+new String(delivery.getBody()));
},(s)->{});
}
}
class ReceiveLogs02{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
//声明一个队列 临时队列 队列名称随机
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"");
System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
channel.basicConsume(queue, true, (s,delivery) ->{
System.out.println("ReceiveLogs02控制台打印:"+new String(delivery.getBody()));
},(s)->{});
}
}
4.5.Direct exchange
直接交换机 路由模式
队列支队他绑定的交换机的消息感兴趣,绑定参数routingKey来表示也可以称该参数为binding key,创建绑定我们用代码channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"routingKey");绑定之后的意义由交换类型决定。
当routhingKey相同时,就是发布订阅模式,当帮懂得routhingKey不同时就是路由模式,也叫直接交换机。
4.5.1 路由模式多重绑定
package com.rabbitmq.six;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class directLogs {
}
class EmitLogs{
public static void main(String[] args) throws IOException, TimeoutException {
int index=0;
String [] routingKey={"info","warning","error"};
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
Scanner sc=new Scanner(System.in);
while (sc.hasNext()){
String msg=sc.next();
channel.basicPublish(RabbitMQConfig.DIRECT_EXCHANGE_NAME,routingKey[index%routingKey.length],null,msg.getBytes());
index++;
System.out.println("生产者发出消息:"+msg);
}
}
}
class ReceiveLogs01{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.DIRECT_EXCHANGE_NAME,BuiltinExchangeType.DIRECT);
//声明一个队列 临时队列 队列名称随机
channel.queueDeclare("console",false,false,false,null);
//绑定交换机和队列
channel.queueBind("console",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"info");
channel.queueBind("console",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"warning");
System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
channel.basicConsume("console", true, (s,delivery) ->{
System.out.println("ReceiveLogs01Console控制台打印:"+new String(delivery.getBody()));
},(s)->{});
}
}
class ReceiveLogs02{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明一个队列 临时队列 队列名称随机
channel.queueDeclare("disk",false,false,false,null);
//绑定交换机和队列
channel.queueBind("disk",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"error");
System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
channel.basicConsume("disk", true, (s,delivery) ->{
System.out.println("ReceiveLogs02控制台打印:"+new String(delivery.getBody()));
},(s)->{});
}
}
此时第一次输入routingKey取到info,因为消费者1绑定的routingKey包含info,此时消费者1接收,第二次输入,routingKey取warning消费者1绑定的routingKey也包含warning,所以第二条数据也被消费者1接收,第三次输入routingKey取error,此时消息被消费者2接收。
4.6.Topic(主题交换机)
4.6.1概念
尽管direct交换机做了某些改进,但是他仍然有局限性,例如:日志类型有“info.base”,“info.advantage”,某个队列只需要“info.base”,那么这个时候direct交换机就做不到了。需要使用topic交换机。
4.6.2 topic要求
类型是topic交换机的信息的routing_key不能随便写,必须满足要求,他必须是一个单词表,以“.”分隔,这些单词可以是任意单词,例如“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”,这种类型的,但是最大不能超过255字节。
在这个规则列表中,有两个替换符需要注意
*(星号)可以替代一个单词
#(井号)可以替代零个或多个单词
对于直接交换机来说,最多只能路由一个队列,可以捆绑多个,但是发送只会发送到一个队列中。
4.6.3 匹配案例
Q1=>绑定的是
1.中间带orange带三个单词的字符串(*.orange.*)
Q2=>绑定的是
1.最后一个单词是rabbit的三个单词(*.*.rabbit)
2.第一个单词是lazy的多个单词(lazy.#)
routing_key 满足队列 接收次数
quick.orange.rabbit Q1.1,Q2.1 2
lazy.orange.elephant Q1.1 1
quick.orange.fox Q1.1 1
lazy.brown.fox Q2.2 1
lazy.pink.rabbit Q2.1,Q2.2 1
quick.brown.fox 无 0
quick.orange.male.rabbit 无 0
lazy.orange.mel.rabbit Q2.2 1
注意:当一个队列绑定#,那么他将接收所有数据,类似于fanout,如果队列绑定键没有#和*那么该队列绑定类型就是direct。所以主题交换机包含了其他两个交换机。
4.6.4 Topic代码
package com.rabbitmq.seven;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicExchange {
}
class EmitLogs{
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
String [] routingKey={"quick.orange.rabbit","lazy.orange.elephant"," quick.orange.fox","lazy.brown.fox","lazy.pink.rabbit","quick.brown.fox","quick.orange.male.rabbit","lazy.orange.mel.rabbit"};
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
for (int i = 0; i < routingKey.length; i++) {
Thread.sleep(1500);
String msg=routingKey[i]+":"+i;
channel.basicPublish(RabbitMQConfig.TOPIC_EXCHANGE_NAME,routingKey[i],null,msg.getBytes());
System.out.println("生产者发出消息:"+msg);
}
}
}
class ReceiveLogs01{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明一个队列 临时队列 队列名称随机
channel.queueDeclare("Q1",false,false,false,null);
//绑定交换机和队列
channel.queueBind("Q1",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"*.orange.*");
System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
channel.basicConsume("Q1", true, (s,delivery) ->{
System.out.println("接收队列:Q1绑定键:"+delivery.getEnvelope().getRoutingKey()+"内容"+new String(delivery.getBody()));
},(s)->{});
}
}
class ReceiveLogs02{
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明一个交换机
channel.exchangeDeclare(RabbitMQConfig.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明一个队列 临时队列 队列名称随机
channel.queueDeclare("Q2",false,false,false,null);
//绑定交换机和队列
channel.queueBind("Q2",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind("Q2",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"lazy.#");
System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
channel.basicConsume("Q2", true, (s,delivery) ->{
System.out.println("接收队列:Q2绑定键:"+delivery.getEnvelope().getRoutingKey()+"内容"+new String(delivery.getBody()));
},(s)->{});
}
}
生产者:
生产者发出消息:quick.orange.rabbit:0
生产者发出消息:lazy.orange.elephant:1
生产者发出消息: quick.orange.fox:2
生产者发出消息:lazy.brown.fox:3
生产者发出消息:lazy.pink.rabbit:4
生产者发出消息:quick.brown.fox:5
生产者发出消息:quick.orange.male.rabbit:6
生产者发出消息:lazy.orange.mel.rabbit:7
消费者1:
ReceiveLogs01等待接收消息,并将消息打印在控制台
接收队列:Q1绑定键:quick.orange.rabbit内容quick.orange.rabbit:0
接收队列:Q1绑定键:lazy.orange.elephant内容lazy.orange.elephant:1
接收队列:Q1绑定键: quick.orange.fox内容 quick.orange.fox:2
消费者2:
ReceiveLogs02等待接收消息,并将消息打印在控制台
接收队列:Q2绑定键:quick.orange.rabbit内容quick.orange.rabbit:0
接收队列:Q2绑定键:lazy.orange.elephant内容lazy.orange.elephant:1
接收队列:Q2绑定键:lazy.brown.fox内容lazy.brown.fox:3
接收队列:Q2绑定键:lazy.pink.rabbit内容lazy.pink.rabbit:4
接收队列:Q2绑定键:lazy.orange.mel.rabbit内容lazy.orange.mel.rabbit:7
4.7.死信队列
4.7.1概念
从概念上来讲,死信,指的是无法被消费的消息,一般来说producer将消息投递到broker或者直接到了queue中,consumer从queue中去除消息就行消费,但是某些时候由于特殊的原因,导致queue中的某些消息无法被消费,这样的消息如果没有后续处理就变成了死信,有私心自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ中的死信队列机制,当消息发生异常时,将消息投入死信队列中,当系统恢复正常时,去除消费。还有比如说:用户在商城下单成功并单击支付后,在指定时间内尚未支付时自动失效。
4.7.2死信的来源
消息TTL过期(可以通过生产者设置,也可以通过消费者设置)
队列达到了最大长度(队列满了,无法在添加到MQ中)
消息被拒绝(basic.reject或者basic.nack)并且不放回队列中(requeue=false)
4.7.3死信代码
当设置TTL为10秒时,超过10秒的消息自动进入死信队列。
当设置最大长度为6时,队列内消息超过6条将进入死信队列。
当设置消息拒绝时,被拒绝的消息自动进入死信队列。
消费者
package com.rabbitmq.eight;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class Consumer01 {
public static final String NORMAL_QUEUE = "normal_queue";
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_QUEUE = "dead_queue";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明普通和死信交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
// 声明死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
// 死信的绑定
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
Map<String, Object> arguments = new HashMap<>();
// 普通队列设置对应的交换机
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// 设置过期时间
// arguments.put("x-message-ttl", 100000);
// 设置死信队列的RouteKey
arguments.put("x-dead-letter-routing-key", "lisi");
// 设置队列最大长度
// arguments.put("x-max-length", 6);
// 声明普通队列
channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
// 普通的绑定
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
DeliverCallback deliverCallback = (consumerTag, message) -> {
String msg = new String(message.getBody());
//消息被拒绝
if (msg.equals("info5")) {
System.out.println("Consumer01接收到消息" + message + "并拒绝签收该消息");
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
} else {
System.out.println("consumer01接收到消息:" + msg);
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> {
});
}
}
class Consumer02 {
public static final String NORMAL_QUEUE = "normal_queue";
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_QUEUE = "dead_queue";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("Consumer02等待接受消息:");
channel.basicConsume(DEAD_QUEUE, false, (s,d)->{
String msg = new String(d.getBody());
System.out.println("consumer01接收到消息:" + msg);
channel.basicAck(d.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {
}); }
}
生产者
package com.rabbitmq.eight;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
//设置过期时间
// AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
System.out.println("发送消息"+message);
}
}
}
4.8.延迟队列
当死信队列中消费者1永久消失,过期时间设置为10s,那么从生产者1到消费者2所花费的时间就是10s,延迟队列就是死信队列中的消息过期这一种情况。
4.8.1 概念
延时队列,队列内部是有虚的,最重要的特性就是体现在他的延时属性上,延时队列中的元素是希望在指定时间到了以后或者之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
4.8.2 使用场景
1.订单在十分钟内未支付自动取消。
2.新创建的店铺,如果10天内没有上传商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,三天内没有得到处理则通知相关人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个参会人员参加会议。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成时间,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎是使用定时任务轮训查询数据,每秒一次,再取出需要被处理的数据。但是如果这种方式面对的试一下对于时间不是严格限制而是宽松意义上的某段时间,那么每天晚上拍个定时任务进行自动结算也是可行的,但是面对数据量比较大,并且时效性较强的场景,如:短期内未支付订单可能达到百万甚至是千万级别,对于如此庞大的数据,人就是用轮训的方式显然是不可取的,因为同一秒内无法完成所有订单的检查,同时给数据库带来很大的严厉,无法满足业务需求而且性能低下。
4.9.整合Spring Boot
4.9.1 新建Springboot项目
4.9.1.1 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
4.9.1.2修改配置文件
spring.rabbitmq.host=112.124.34.53
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
4.9.1.3添加swagger配置类
package com.rabbitmq02.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @ClassName TtlQueueConfig
* @Author LY
* @Date 2023/11/6 14:24
**/
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X_EXCHANGE="X";
//死信交换机名称
public static final String Y_DEAD_EXCHANGE="Y";
//普通队列名称
public static final String QUEUE_A="QA";
public static final String QUEUE_B="QB";
//死信队列名称
public static final String DEAD_QUEUE_D="QD";
//声明直接交换机X
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明死信交换机Y
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_EXCHANGE);
}
//声明称普通队列QA
@Bean("queueA")
public Queue queueA(){
Map<String, Object> arg=new HashMap<>(3);
//死信交换机
arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
//死信RoutingKey
arg.put("x-dead-letter-routing-key","YD");
//过期时间TTL
arg.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUE_A).withArguments(arg).build();
}
//声明称普通队列QB
@Bean("queueB")
public Queue queueB(){
Map<String, Object> arg=new HashMap<>(3);
//死信交换机
arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
//死信RoutingKey
arg.put("x-dead-letter-routing-key","YD");
//过期时间TTL
arg.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE_B).withArguments(arg).build();
}
//声明称死信队列QD
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_QUEUE_D).build();
}
//绑定queueA和xExchange
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange
xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//绑定queueB和xExchange
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange
xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
//绑定queueD和yExchange
@Bean
public Binding queueDBindingy(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange
yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
4.9.1.4 新增生产者
package com.rabbitmq02.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/***
* @Description
* 发从延迟消息
* @ClassName SendMessageController
* @Author LY
* @Date 2023/11/6 15:06
**/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
//开始发消息
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间={},发送一条信息给两个ttl队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+new String(message.getBytes()) );
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+new String(message.getBytes()) );
}
}
4.9.1.5新增消费者
package com.rabbitmq02.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/***
* @Description
* TTL消费者
* @ClassName DeadLetterQueueConsumer
* @Author LY
* @Date 2023/11/6 15:39
**/
@Component
@Slf4j
public class DeadLetterQueueConsumer {
//接收消息
@RabbitListener(queues = "QD")
private void receiveD(Message message, Channel channel){
String msg=new String(message.getBody());
log.info("当前时间:{},死信队列消息:{}",new Date().toString(),msg);
}
}
4.9.1.6结果
请求地址:http://localhost:8080/ttl/sendMsg/123456789987456321
当前时间=Mon Nov 06 16:30:42 GMT+08:00 2023,发送一条信息给两个ttl队列:123456789987456321
当前时间:Mon Nov 06 16:30:52 GMT+08:00 2023,死信队列消息:消息来自ttl为10s的队列:123456789987456321
当前时间:Mon Nov 06 16:31:22 GMT+08:00 2023,死信队列消息:消息来自ttl为40s的队列:123456789987456321
可以看到 延时接收已经生效了,但是这个代码扩展性并不好,如果我们现在需要新增一个1小时以后得延时队列,还需要新建一个队列,重新建立链接等等,如果有无数个延时需求,就需要无数个队列来满足需求,所以扩展性并不好,也不现实。
4.9.2 延时队列优化
基于上述问题,我们应该创建一个通用的延迟队列,不设置过期时间,具体过期时间应该由生产者发消息时进行指定。这样就可以用一个延迟队列实现所有延迟需求。
4.9.2.1 新增config类QueueC
QueueC不设置过期时间,生命队列并绑定。
//声明称普通队列QC
@Bean("queueC")
public Queue queueC(){
Map<String, Object> arg=new HashMap<>(2);
//死信交换机
arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
//死信RoutingKey
arg.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C).withArguments(arg).build();
}
//绑定queueC和xExchange
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange
xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
4.9.2.2 创建生产者
创建生产者发送消息并设置过期时间
//开始发消息 以及TTL
@GetMapping("/sendExpiratMsg/{message}/{ttlTime}")
public void sendExpiratMsg(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间={},发送一条过期市场为:{}ms的信息给队列QC,信息:{}",new Date().toString(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message,msg->{
//设置发送消息的延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
} );
}
当前时间=Mon Nov 06 16:55:01 GMT+08:00 2023,发送一条过期市场为:200
当前时间:Mon Nov 06 16:55:04 GMT+08:00 2023,死信队列消息:你好2
当前时间=Mon Nov 06 16:55:08 GMT+08:00 2023,发送一条过期市场为:200
当前时间:Mon Nov 06 16:55:28 GMT+08:00 2023,死信队列消息:你好2
结果已经达到了动态设置过期时间。
4.9.2.3 注意
如果我们先发送20秒的消息你好1,然后发送2秒的消息你好2,他并不会先接收到你好2,因为消息队列只会检测第一条信息是否过期,并不会检测第二条信息是否过期,所以你好2会在你好1被接收后紧接着被接收。这是死信队列巨大的问题,因为你好1的时间不应该约束到你好2。
当前时间=Mon Nov 06 16:54:10 GMT+08:00 2023,发送一条过期市场为:20000
当前时间=Mon Nov 06 16:54:14 GMT+08:00 2023,发送一条过期市场为:2000m
当前时间:Mon Nov 06 16:54:30 GMT+08:00 2023,死信队列消息:你好1
当前时间:Mon Nov 06 16:54:30 GMT+08:00 2023,死信队列消息:你好2
4.9.3 RabbitMQ插件实现延迟队列
只要是基于死信队列的,上述问题都没办法处理,所以只能基于插件实现延迟队列。
4.9.3.1 下载插件
插件地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/
4.9.3.2 进入目录
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.*.*/plugins
4.9.3.3 上传文件
4.9.3.4 安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
4.9.3.5 重启MQ
service rabbitmq-server restart
4.9.3.6 新建交换机
安装成功之后,新建交换机,发现type多了一个延迟消息类型。所以延迟消息不再由队列来实现,而是由交换机来实现。中间省去了死信队列的步骤。
4.9.3.7 新增配置类
package com.rabbitmq02.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/***
* @Description
* 给予插件的延迟队列
* @ClassName DelayedQueueConfig
* @Author LY
* @Date 2023/11/7 10:06
**/
@Configuration
public class DelayedQueueConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME="delayed.exchange";
//队列
public static final String DELAYED_QUEUE_NAME="delayed.queue";
//ROUTINGKEY
public static final String DELAYED_ROUTING_KEY="delayed.routingkey";
//声明队列
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
//声明交换机 基于插件
@Bean
public CustomExchange delayedExchange(){
/**
* 参数
* 1.交换机名称
* 2.交换机类型
* 3.是否需要持久化
* 4.是否需要自动删除
* 5.自定义参数
*
*/
Map<String,Object> arg=new HashMap<>();
arg.put("x-delayed-type","direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arg);
}
//绑定
@Bean
public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue,@Qualifier("delayedExchange") CustomExchange delayedExchange) {
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
4.9.3.8 生产者
//基于插件的延时消息
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendDelayMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间={},发送一条延时时间为:{}ms给队列delayed.queue,信息:{}",new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg->{
//设置发送消息的延迟时长
msg.getMessageProperties().setDelay(delayTime);
return msg;
} );
}
9.3.8 消费者
@Component
@Slf4j
public class DelayQueueConsumer {
//接收消息
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
private void receiveDelayedQueue(Message message ){
String msg=new String(message.getBody());
log.info("当前时间:{},延迟队列消息:{}",new Date().toString(),msg);
}
}
4.9.3.9 结论
基于插件的延迟消息可以做到完全根据发送消息延迟时间来进行延迟而不受消息顺序影响
当前时间=Tue Nov 07 10:53:45 GMT+08:00 2023,发送一条延时时间为:20000ms给队列delayed.queue,信息:你好1
当前时间=Tue Nov 07 10:53:49 GMT+08:00 2023,发送一条延时时间为:2000ms给队列delayed.queue,信息:你好2
当前时间:Tue Nov 07 10:53:51 GMT+08:00 2023,延迟队列消息:你好2
当前时间:Tue Nov 07 10:54:05 GMT+08:00 2023,延迟队列消息:你好1
4.9.4 总结
延时队列在需要延时处理的情况下非常有用,使用RabbitMQ来实现延迟队列可以很好的利用RabbitMQ的特性,如:消息可靠发送,消息可靠投递,死信队列来保障消息至少被消费一次,以及未被正确处理的消息不会被丢弃,另外通过RabbitMQ的集群特性,可以很好的解决单点故障问题,不会因为单节点挂掉导致延时队列不可用或者消息丢失。
当然延时队列还有其他很多西安则,例如JAVA的DelayQueue,利用Redis的zset利用Quartz或者kafka的时间轮,这些方式各有特点,具体要看使用的场景
5 发布确认高级
生产环境由于某些不明原因,导致rabbitMQ重启,在RabbitMQ重启期间生产者投递消息失败,导致消息丢失,需要手动处理和回复。于是我们考试思考,如何才能进行RabbitMQ的可靠投递。特别是在极端的情况下。RabbitMQ集群不可用的时候,无法投递的消息该如何处理呢。
5.1发布确认SpringBoot版
5.1.1确认机制方案
当交换机丢失,发送的消息自然就丢失了,交换机存在,队列丢失了,交换机又无法投递到队列,此时消息依然会被丢弃。
所以生产者将消息发送给交换机或者队列(MQ),无论交换机无法收到,亦或者无法投递给队列,生产者都应该将消息存入缓存中,然后采用定时任务对失败的消息重新发送。
5.1.2 代码架构图
这种架构会有两种问题:1:交换机出现问题。2:队列出现问题。
5.1.3 生产者
package com.rabbitmq02.controller;
import com.rabbitmq02.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/confirm")
@RestController
@Slf4j
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
//发消息
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY+"11",message,correlationData);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"123",ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("发送消息内容:{}",message);
}
}
5.1.4 消费者
package com.rabbitmq02.consumer;
import com.rabbitmq02.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/***
* @Description
* 发布确认高级 消费者
* @ClassName ConfirmConsumer
* @Author LY
* @Date 2023/11/7 11:37
**/
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void confirmMsg(Message message){
String msg= new String(message.getBody());
log.info("{}队列,接收到的消息内容:{}",ConfirmConfig.CONFIRM_QUEUE_NAME,msg);
}
}
5.1.5 增加配置
在application.properties中添加
spring.rabbitmq.publisher-confirm-type=correlated
有三个可选模式
NONE:
禁用发布确认模式(默认)
CORRELATED:
发布消息成功到交换器后会触发回调方法
SIMPLE:
经测试有两种效果:
1.和CORRELATED一样,发布消息成功到交换器后会触发回调方法。
2.发布消息成功后使用RabbitTemplate调用waitFprConfirms或者waitForConfirmsOrDie方法等到broker节点返回发送结果,根据返回结果判定喜爱一步逻辑。要注意的是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker。相当于单个确认。
5.1.6 回调接口
当生产者消息发送成功以后,并不能感知到消息是否发送成功,所以应当提供一个会点接口,供消息投递成功以后来回调。
package com.rabbitmq02.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
//缺少入住回RabbitTemplate,如果不注入,那还是钓不到已经修改过的接口
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 注入
* PostConstruct注解在其他注解完成后才会执行
*/
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息交换机接收成功
* @param correlationData 保存消息的id及相关的信息
* @param b 交换机是否收到消息 true
* @param s 原因 null
* 2.发消息交换机接收失败
* @param correlationData 保存消息的id及相关的信息
* @param b 交换机是否收到消息 false
* @param s 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData==null?"":correlationData.getId();
if (b){
log.info("交换机已经收到了消息,id:{}",id);
}else {
log.info("交换机未收到消息,id:{},原因:{}",id,s);
}
}
}
5.1.7 结果分析
正确的交换机,正确的routingKey,交换机收到了消息,队列接收到了消息,调用了正确的回调。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key1
confirm.queue队列,接收到的消息内容:你好2
正确的回调函数,交换机已经收到了消息,id:1
错误的交换机,正确的routingKey,交换机未收到消息,队列也为未收到消息,并有错误的回调。
交换机:confirm.exchange123,发送消息内容:你好2,routingKey:key1
Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange123' in vhost '/', class-id=60, method-id=40)
错误的回调函数,交换机未收到消息,id:1,原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange123' in vhost '/', class-id=60, method-id=40)
正确的交换机,错误的的routingKey,交换机接收到了消息,队列未收到消息,调用了正确的回调。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key111
正确的回调函数,交换机已经收到了消息,id:1
所以该方式只能确保消息正确到达了交换机,而不发保证是否真正的被队列所接收。
5.2回退消息
5.2.1 Mandatory参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给生产者发送确认消息,如果发现该消息不可路由(无法发送到队列),那么该消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。通过设置Mandatory参数可以将消息在传递过程中不可到达目的地的消息返回给生产者。
5.2.2 新增配置
spring.rabbitmq.publisher-returns=true
5.2.3 新增配置
可以实现RabbitTemplate.ReturnCallback接口或者RabbitTemplate.ReturnsCallback接口。
package com.rabbitmq02.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
//缺少入住回RabbitTemplate,如果不注入,那还是钓不到已经修改过的接口
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 注入
* PostConstruct注解在其他注解完成后才会执行
*/
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息交换机接收成功
* @param correlationData 保存消息的id及相关的信息
* @param b 交换机是否收到消息 true
* @param s 原因 null
* 2.发消息交换机接收失败
* @param correlationData 保存消息的id及相关的信息
* @param b 交换机是否收到消息 false
* @param s 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData==null?"":correlationData.getId();
if (b){
log.info("正确的回调函数,交换机已经收到了消息,id:{}",id);
}else {
log.info("错误的回调函数,交换机未收到消息,id:{},原因:{}",id,s);
}
}
/**
* 生产者发消息 如果消息没有被对应的交换机进队列
* 就会把消息回退给生产者 进行重发
* 在发送消息的过程中不可达目的地时将消息返回给生产者
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息{},被交换机{}退回,退回的原因是{},路由key是{}",
new String(message.getBody()),
exchange, replyText, routingKey);
}
/* @Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息{},被交换机{}退回,退回的原因是{},路由key是{}",
new String(returnedMessage.getMessage().getBody()),
returnedMessage.getExchange(), returnedMessage.getReplyText(), returnedMessage.getRoutingKey());
}
*/
}
5.2.4 结果分析
正确的交换机,正确的routingKey,交换机接收成功,队列接收成功,消费者接收成功,正确的回调。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key1
confirm.queue队列,接收到的消息内容:你好2
正确的回调函数,交换机已经收到了消息,id:1
正确的交换机,错误的routingKey,交换机接受成功,队列未接收,消费者未接收,正确的回调函数,消息被退回给生产者。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key111
消息你好2,被交换机confirm.exchange退回,退回的原因是NO_ROUTE,路由key是key111
正确的回调函数,交换机已经收到了消息,id:1
5.3 备份交换机
有了mandatory参数和回退消息,我们获得了对无法投递消息的感知能力,有机会再生产这的消息无法被投递时发现并处理。但是有时候,我们并不知道无法处理这些消息,最多打印一下日志,然后触发警报,手动处理。而且无形之中增加了生产者的复杂性,如果生产者在多个服务器的时候,手动复制日志也容易出错。
而几不想增加生产者的复杂性,又想处理这些无法被路由的消息,我们可以设置一个交换机为另一个交换机的备份交换机,当见换机收到一条无法路由的消息时,把该条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机类型为Fanout,这样就能把所有的消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样原本交换机所有无法路由的消息都会进入到该队列中,也可以在新建一个报警队列,用独立的消费者来进行监测和报警
5.3.1 代码架构图
5.3.2 修改配置文件
在高级发布确认模式的基础上,新增备份交换机,备份队列,报警队列
package com.rabbitmq02.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/***
* @Description
* 发布确认 高级 配置类
* @ClassName ConfirmConfig
* @Author LY
* @Date 2023/11/7 11:25
**/
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
//队列
public static final String CONFIRM_QUEUE_NAME="confirm.queue";
//ROUTINGKEY
public static final String CONFIRM_ROUTING_KEY="key1";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME="backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME="backup.queue";
//报警队列
public static final String WARNING_QUEUE_NAME="warning.queue";
//创建备份交换机
@Bean
public FanoutExchange buckupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明交换机
@Bean
public DirectExchange confirmExchange(){
//return new DirectExchange(CONFIRM_EXCHANGE_NAME);
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
//声明队列
@Bean
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE_NAME);
}
//创建备份队列
@Bean
public Queue backupQueue(){
return new Queue(BACKUP_QUEUE_NAME);
}
//创建报警队列
@Bean
public Queue warningQueue(){
return new Queue(WARNING_QUEUE_NAME);
}
//绑定
@Bean
public Binding bindconfirmExchangeToConfirmQueue(@Qualifier("confirmExchange") DirectExchange confirmExchange,@Qualifier("confirmQueue") Queue confirmQueue){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
//绑定备份交换机和备份队列
@Bean
public Binding buckupExchangeBindToBackupQueue(@Qualifier("buckupExchange") FanoutExchange buckupExchange,@Qualifier("backupQueue") Queue backupQueue){
return BindingBuilder.bind(backupQueue).to(buckupExchange);
}
//绑定备份交换机和报警队列
@Bean
public Binding buckupExchangeBindToWarningQueue(@Qualifier("buckupExchange") FanoutExchange buckupExchange,@Qualifier("warningQueue") Queue warningQueue){
return BindingBuilder.bind(warningQueue).to(buckupExchange);
}
}
5.3.3 新增报警消费者
package com.rabbitmq02.consumer;
import com.rabbitmq02.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/***
* @Description
* 发布确认高级 消费者
* @ClassName ConfirmConsumer
* @Author LY
* @Date 2023/11/7 11:37
**/
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void confirmMsg(Message message){
String msg= new String(message.getBody());
log.info("{}报警队列队列,接收到的消息内容:{}",ConfirmConfig.WARNING_QUEUE_NAME,msg);
}
}
5.3.4 结果分析
正确的交换机,正确的routingKey还是正常接收。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key1
正确的回调函数,交换机已经收到了消息,id:1
confirm.queue队列,接收到的消息内容:你好2
正确的交换机,错误的routingKey,则通过备份交换机,被发到了报警队列中。
交换机:confirm.exchange,发送消息内容:你好2,routingKey:key111
warning.queue报警队列队列,接收到的消息内容:你好2
mandatory参数与备份交换机一起使用的时候,备份交换机优先级较高。两者同时存在不会被退回到生产者。
6.RabbitMQ其他知识点
6.1幂等性
6.1.1概念
用户对于同一操作发起的一次货多次请求的结果是一致的,不会因为多次点击而产生副作用。例如支付,用户购买商品后支付,支付扣款成功,但是返回结果的时网络异常,此时钱已经扣了,用户再次点击,此时会第二次扣款,返回结果成功,此时用户被扣款两次,流水记录也是两次,在以前的系统中,我们只需要把数据操作放入事物中即可,发生错误立即回滚,但是响应客户端的时候也可能出现某些问题。
6.1.2 消息重复消费
消费者在消费MQ中的消息时,MQ已经把消息发送给消费者,消费者再给MQ返回ack时网络中断,故MQ未收到确认消息,该消息会被转发给其他消费者,或网络重连后重新发给该消费者,但实际上该消费者已经成功消费了该消息,造成了消费者消费了重复的消息。
6.1.3 解决思路
MQ消费者的幂等性问题解决一般使用全局ID,或者写一个唯一标识等等,每次消费时都先通过该id或标识判断是否已经消费过。
6.1.4 消费端的幂等性保障
在业务高分时期,生产端可能重复发送了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,业内主流的幂等性操作有两种:A.唯一ID+指纹码机制。B.利用redis的原子性去实现。
6.1.5 唯一ID+指纹码
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,但他不一定是我们系统生成的,基本都是有我们业务规则拼接而来,但必须要保证唯一性,然后利用查询语句进行判断这个id是否已存在,优势是实现简单,就一个拼接,然后查询判重。劣势是在高并发时如果是单个数据库就会有写入性瓶颈,也可以用分库分表提升性能,但是并不推荐。
6.1.6 Redis原子性
利用redis执行setnx命令,天然具有幂等性判断,从而实现不重复消费。
6.2 优先级队列
6.2.1 使用场景
普通商城系统中,有一个订单催促功能,例如客户下单后,特定时间内未付款,就会发送短信提醒,但是商家肯定要区分一些大商家跟小商家,大商家给带来的利润相对较大,所以他们的订单理所应当的优先处理,之前都是使用Redis的List做一个简单的消息队列,并不能实现优先级的场景。
所以当订单量大了以后,采用RabbitMQ进行改造和优化,如果是大客户推给一下相对比较高的有衔接,否则就是默认优先级。
6.2.2 如何添加
6.2.2.1 页面添加
优先级取值可以是0-255 太大会造成性能下降。
6.2.2.2 代码添加
生产者:
给5号设置为最高优先级
package com.rabbitmq.priorityQueue;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class Producer {
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
Map<String,Object> arg=new HashMap<>();
arg.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,false,false,false,arg);
for (int i = 1; i <= 20; i++) {
String massage="hello word"+i;
if (i==5){
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties, massage.getBytes());
}else {
channel.basicPublish("",QUEUE_NAME,null, massage.getBytes());
}
System.out.println("消息已发送完毕"+massage);
}
}
}
消费者:
package com.rabbitmq.priorityQueue;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class Consumer01 {
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
DeliverCallback deliverCallback=(consumerTag,massage)->{
System.out.println(new String(massage.getBody()));
};
//声明参数4 取消消息接收时的回调
CancelCallback cancelCallback= consumerTag->{
System.out.println("消息接收被中断");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
6.2.2.3 结论
5被第一个取出
hello word5
hello word1
hello word2
hello word3
6.3 惰性队列
正常情况下消息是保存在内存中的,但是队形队列的消息是保存在磁盘中的。效率并不高。
6.3.1 使用场景
RabbitMQ从3.6.0版本开始引入了队形队列的概念,惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,他的一个重要设计目标是支持更长的队列,既更多的消息存储,当消费者由于各种各样的原因(宕机,关闭等)导致长时间不能消费消息,造成消息堆积时,惰性队列就很有必要了。
默认情况下,生产者将消息发送到MQ时,队列中的消息会尽可能存到内存中,这样可以更快速的将消息发送给消费者,即使是持久化的信息,再被写入磁盘中同时也会在内存中驻留一份备份。当MQ释放内存的时候才会将内存中的消息写入磁盘中,这个操作消耗时间过长,也会阻塞队列操作,进而无法接受新的消息。MQ的开发者们一直在升级相关算法,但是效果始终不太理想,尤其是在信息量特别大的时候。
6.3.2 两种模式
队列具有两种模式:default和lazy,默认的为default模式,在3.6.0之前无需做任何变更。lazy模式即为惰性队列模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用两种方式设置的话,Policy的方式有更高的优先级,如果要通过声明的方式改变已有队列的话,只能先删除已有队列,再重新声明。
5.3.2.1 客户端设置
5.3.2.2 代码设置
队列生命的时候可以通过“x-queue-mode”参数来设置队列模式,取值为“default”和“lazy”:
Map<String,Object> arg=new HashMap<>();
arg.put("x-queue-mode","lazy");
channel.queueDeclare(QUEUE_NAME,false,false,false,arg);
6.3.3 内存开销对比
在发送100w条数据的情况下,普通队列消耗内存1.2g,惰性队列仅仅占用1.2m。
7 RabbitMQ 集群
7.1 使用集群的原因
RabbitMQ遇到内存崩溃,机器掉电或者主板故障等问题或者单台MQ服务器可以满足1000条消息吞吐量,但是需要10w条消息吞吐量,够慢昂贵的服务器来增强MQ性能并不可取,所以可以选择搭建集群来解决实际问题。
7.1.2 搭建集群
可参考其他文章
7.2 镜像队列
如果RabbitMQ集群中只有一个Broker节点,那么该节点失效将导致整体服务临时性不可用,并且可能导致消息丢失,就算将消息设置为持久化,对应队列也持久化,仍然无法避免消息发送后和写入磁盘之前出现问题。
引入镜像队列(Mirror Queue)机制,可以将队列镜像到集群中的其他Broker节点上,如果集群中的一个节点失效了,就可以自动切换到其他节点上,保证服务的可用性。
7.2.1 搭建步骤
可参考其他文章
7.3 Haproxy+Keepalive 高负载高可用
当有一个队列无法连接,他的镜像队列可以连接,但是生产者只会连接到该无法连接该镜像队列,此时MQ无法处理,但是可以使用Haproxy+Keepalive 实现高可用。
HAProxy提供高可用性,负载均衡以及基于TCP/HTTP应用的代理,支持虚拟主机,他是免费,快速并可靠的一种解决方案,包括Twiter,Reddit,StackOverflow,GitHub在内的多家知名公司正在使用,Haproxy实现了一种事件驱动,单一进程模型,此模型支持非常大的并发连接数。
7.3.1 搭建步骤
可参考其他文章
7.3.2 扩展
nginx,lvs,Haproxy,之间的区别:
www.ha97.com/5646.html
7.4 Federation Exchange
联邦交换机,联合交换机
7.4.1 概念
当我们有两个Broker,彼此之间相距较远,离Broker1近的客户端,应当访问Broker1,但是Broker1和Broker2之间也应当进行数据同步,否则离Broker1近的客户端无法获取到Broker2内的数据。
7.4.2 原理
7.5 Federation Queue
7.5.1 概念
联邦队列可以再多个Broker节点或者集群之前为单个队列提供负载均衡功能,一个联邦队列可以连接一个或多个上游队列,并从这些上游队列中获取消息,以满足本地消费者消费消息的需求。
7.5.2 原理
7.6 shovel
7.6.1 概念
Federation具备类似数据转发的功能,shovel能够可靠,持续的从一个Borker中的队列(作为源端:source)拉去数据并转发至另一个Borker中的交换器(作为目的端:destination)。作为源端的队列和作为目的端的交换器可以同时位于同一个Broker也可以在不同的Broker上。Shovel可以翻译为“铲子”,是一种比较形象的比喻,铲子可以将消息从一方铲向另一方。shovel行为就像优秀的客户端程序,能够负责连接源和目的地,负责消息的读写及负责连接失败问题的处理。
7.6.2 原理
7.6.1 搭建步骤
可参考其他文章
完.