RabbitMQ
- 一:MQ的相关概念
- 二:Linux下RabbitMQ的安装
- 二:RabbitMQ的生产者和消费者
- 三:Exchanges(交换机)
- 四:死信队列
- 五:延迟队列
- 六:发布确认(高级 )
- 七:Rabbitmq的其他知识点
- 八:Rabbitmq的集群
- 九:Federation Exchang(联邦交换机)
- 十:Springboot实现Topic交换机
- 十二:消息的一致性
一:MQ的相关概念
1.1 什么是MQ
- MQ从本质上来说是一个队列,FIFO先进先出,只不过队列中存放的是message而已,还是一种跨进程的通讯机制,用于上下游传递消息,在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦和+物理解耦”`的消息通讯服务,使用了MQ之后,消息发送上游只需要依赖MQ, 不用依赖其他的服务
1.2 RabbitMQ MQ的好处
- 1)流量消峰
- 订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正
常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限
制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分
散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体
验要好。 - .2)应用解耦
应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合
调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于
消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在
这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流
系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性
- 异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可
以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api,
B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,
A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此
消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不
用做这些操作。A 服务还能及时的得到异步处理成功的消息。
1.2 RabbitMQ MQ的概念
RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包
裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是
一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,
存储和转发消息数据。
二:Linux下RabbitMQ的安装
- 官方网站 https://www.rabbitmq.com/download.html
- RabbitMq的安装是依赖于Erlang的环境
- 本次演示cenos7
- 1.将需要安装的软件拖进来
- 需要提前安装erlang语言
- 然后安装rabbitmqserver
需要你的cenos7支持el7
#输入命令安装erlang
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
#给自己的虚拟机安装rabbitma的依赖
yum install socat -y
#安装rabbitmq
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
- 添加开机启动 RabbitMQ 服务
chkconfig rabbitmq-server on
- 启动mq
/sbin/service rabbitmq-server start
- 查看启动状态
/sbin/service rabbitmq-server status
- 安装web管理插件
#先停止
/sbin/service rabbitmq-server stop
#停止完之后安装插件
rabbitmq-plugins enable rabbitmq_management
#启动
/sbin/service rabbitmq-server start
- 在win10的浏览器执行虚拟机ip+端口
- 如果访问不进去,可能是虚拟机的防火墙没有关,关闭之后在win10的cmd窗口运行ping 虚拟机ip,可以ping通就可以
- 默认密码guest guest
- 查看拥有哪些用户可以登录
rabbitmqctl list_users
- 添加登录的账户
rabbitmqctl add_user admin 123
- 设置用户的角色是超级管理员
rabbitmqctl set_user_tags admin administrator
- 设置用户的权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
- 用admin 132就可以登录了
二:RabbitMQ的生产者和消费者
2.1简单队列模式的生产者和消费者
2.1.1 pom文件
<dependencies>
<!--rabbitmq 依赖客户端-->
<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>
</dependencies>
2.1.2 生产者代码
package com.rj.bd.one;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 生产者代码
* @time 2022--11--13--15:32
*/
public class produce {
//定义队列名称
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建工厂
ConnectionFactory factory = new ConnectionFactory();
//设置工厂的ip,连接rabbitmq
factory.setHost("192.168.116.132");
//设置连接的用户名和密码
factory.setUsername("admin");
factory.setPassword("123");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
/**生成一个队列
*1.第一个参数:队列名称
* 2.第三个参数:队列里面的消息是否持久化(磁盘),默认是存在内存中的
*3.第三个参数:该队列是否只供一个消费者进行消费;是否进行消息的共享,true是表示可以有多个,false是只能一个
* 4.是否自动删除,最后一个消费者离开以后该队列是否直接删除,true是直接删除,false是不删除
* 5.其他参数
*/
channel.queueDeclare(QUER_NAMEA,false,false,false,null);
//发消息
String message="hello word"; //消息内容
/**
* 1.第一个参数表示发送到哪个交换机
* 2.路由的key是哪个,队列的名称
* 3.其他的参数
* 4.发送的消息
*/
channel.basicPublish("",QUER_NAMEA,null, message.getBytes());
System.out.println("消息发送完毕");
}
}
2.1.3 消费和代码
package com.rj.bd.one;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 消费者
* @time 2022--11--13--15:51
*/
public class Consumer {
//定义队列名称
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建工厂
ConnectionFactory factory = new ConnectionFactory();
//设置工厂的ip,连接rabbitmq
factory.setHost("192.168.116.132");
//设置连接的用户名和密码
factory.setUsername("admin");
factory.setPassword("123");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//lanmd表达式声明接收消息
DeliverCallback callback=(consumerTag ,message) -> {
String str = new String(message.getBody());
System.out.println(str);
};
//声明取消消息
CancelCallback cancelCallback=(var1) -> {
System.out.println("消费消息失败了,被中断了");
};
/**
* 消费者消费消息
* 第一个参数:消费哪个队列
* 第二个参数:消费成功之后是否需要自动应答,自动应答设置为true
*第三个:消费者取消消费的回调
* 第四个参数:消费者未成功消费的一个回调
*/
channel.basicConsume(QUER_NAMEA,true,callback,cancelCallback);
}
}
2.2工作队列模式的生产者和消费者
- 工作队列(又称为任务队列)的主要思想是避免立即执行的密集型任务,而不得不等待他完成,相反我们安排任务在之后执行,我们将任务封装为消息将其发送到队列,在后台运行的工作进程将弹出任务并最终执行作业,当有多个工作线程时,这些工作线程将一起处理这些任务
2.2.1 抽取工具类
- 将拢余的代码抽出来
package com.rj.bd.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;
/**
* @author LXY
* @desc
* @time 2022--11--13--16:49
*/
public class RabbitMqUtils {
public Channel getChannel () throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
//设置工厂的ip,连接rabbitmq
factory.setHost("192.168.116.132");
//设置连接的用户名和密码
factory.setUsername("admin");
factory.setPassword("123");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
return channel;
}
}
2.2.2 消费者代码
package com.rj.bd.two;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 这是一个工作线程相当于消费者
* @time 2022--11--13--16:51
*/
public class work01 {
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//获取工具类的信道
Channel channel = RabbitMqUtils.getChannel();
//成功接收消息
DeliverCallback deliverCallback=(str ,message) ->{
System.out.println("接收到的消息:"+new String(message.getBody()));
};
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
channel.basicConsume(QUER_NAMEA,true,deliverCallback,callback);
}
}
- 启动
2.2.3 生产者代码
package com.rj.bd.two;
import com.rabbitmq.client.Channel;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc
* @time 2022--11--13--17:02
*/
public class Task01 {
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUER_NAMEA,false,false,false,null);
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
System.out.println("请输入:");
String message=scanner.next();
channel.basicPublish("",QUER_NAMEA,null, message.getBytes());
System.out.println("消息发送成功-------");
}
}
}
2.2.4 测试
2.3消息应答机制
- 消费者完成一个任务需要一定的时间,如果一个消费者完成了任务仅完成了一半就挂了,会发生什么情况,Rabbitmq一旦向消费者传递了一条消息,即立刻将消息标记为删除,这种情况下,突然有一个消费者挂了,我们将丢失正在处理的消息,以及后续发送给消费者的消息,因为它无法接受到
- 为了保证消息不会丢失Rabbitmq引入了消息应答机制,消费者在接收消息并处理之后,告诉rabbitmq说消息已经处理了,rabbitmq就可以将消息删除了
2.3.1 自动应答
- 消息发送之后立即认为消息已经成功接受,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果在消息接受成功之前消费者那边挂了,消息就丢失了,当然另一方面这种模式可能使得接受太多来不及处理的消息形成了一个积压,最终使得内存耗尽,之中被系统杀死,这种模式仅适用于在消费者可以高效并以某种速率能够处理这些消息的情况下使用
2.3.2 消息应答的方式
- 1.Channel.basicAck(用于肯定确认)
Rabbitmq已知道该消息并且成功处理的消息,可以将其丢弃了 - 2.Channel.basicNack(用于否定确认)
- 3.Channel.basicReject(用于否定确认)
与 Channel.basicNack 相比少一个参数
不处理该消息了直接拒绝,可以将其丢弃了
2.3.3Mutiple(批量应答)的解释
- 手动应答的好处是可以批量应答减少网络的拥堵
- true和false代表的不同意思
- true代表应答channel上未应答的消息
比如说有1,2,3,这三条消息
此时到了3了,那么1-2的消息就会被确认收到消息应答 - false
只会应答3的消息,其他的消息不会被确认收到消息应答
2.4 消息自动入队
- 如果消费者由于某些原因,时区连接(通道已关闭,连接已关闭或者TCP丢失)导致消息未发送ACK确认,Rabbitmq了解到消息未完全处理,并将对其进行重新排队,如果此时其他消费者可以去处理,他将很快将其发送给另一个消费者,这样即使某个消费者偶尔死亡,也可以确保消息不会丢失
2.5代码实现
-
开了一个生产者,两个消费者,其中一个消费者睡3s(较快),其中一个消费者睡10s(较慢)
-
首先生产者发送了4条数据
-
work4沉睡3s,接受完毕2个消息
-
- work3沉睡10s,未接受完毕2个消息
- work3沉睡10s,未接受完毕2个消息
-
将沉睡10s的wok3挂掉
-
丢失的数据dd会在word4上运行,从而实现了手动确认的数据安全性不会保证数据丢失
-
生产者
package com.rj.bd.four;
import com.rabbitmq.client.Channel;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc ACK消息确认--确保数据不被应答宕机数据重新分发处理
* @time 2022--11--14--21:45
*/
public class Task2 {
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUER_NAMEA,false,false,false,null);
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()) {
String str=scanner.next();
channel.basicPublish("", QUER_NAMEA, null, str.getBytes());
System.out.println("消息发送成功,发送的消息为"+str);
}
}
}
- 消费者1
package com.rj.bd.four;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import static java.lang.Thread.*;
/**
* @author LXY
* @desc
* @time 2022--11--14--21:49
*/
public class work3 {
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("work3----沉睡10s");
//消息成功应答
DeliverCallback deliverCallback=( var1, message) ->{
try {
sleep(10000); //睡眠10s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消息接受成功,接受信息为:"+new String(message.getBody()));
//第一个参数:对于哪个消息确认应答
//第二个消息:是否批量确认
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("确认完成");
};
//消息失败
CancelCallback callback=( varl) ->{
System.out.println("消息异常");
};
boolean Ack=false; //手动应答
channel.basicConsume(QUER_NAMEA,Ack,deliverCallback,callback);
}
}
- 消费者2
package com.rj.bd.four;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import static java.lang.Thread.sleep;
/**
* @author LXY
* @desc
* @time 2022--11--14--21:49
*/
public class work4 {
public static final String QUER_NAMEA="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("work4----沉睡3s");
//消息成功应答
DeliverCallback deliverCallback=( var1, message) ->{
try {
sleep(3000); //睡眠10s
} catch (InterruptedException e) {
e.printStackTrace();
}
//第一个参数:对于哪个消息确认应答
//第二个消息:是否批量确认
System.out.println("消息接受成功,接受信息为:"+new String(message.getBody()));
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("确认完成");
};
//消息失败
CancelCallback callback=( varl) ->{
System.out.println("消息异常");
};
boolean Ack=false; //手动应答
channel.basicConsume(QUER_NAMEA,Ack,deliverCallback,callback);
}
}
2.6 消息得到持久化
- 刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消
息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列
和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标
记为持久化。
2.6.1 队列开启持久化
- 之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果
要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化 - 生产者代码开启队列持久化,将参数设置为true
- 有一个D就是代表本队列已经开启了持久化
2.6.2 消息开启持久化
- 队列持久化了消息不持久化并不会保证消息不会丢失,所以消息也需要开启持久化操作
- 要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添
加这个属性。
- 将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是
这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没
有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要
更强有力的持久化策略,参考后边发布确认章节。
2.7 不公平分发
-
在最开始的 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是
很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2
处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间
处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是
RabbitMQ 并不知道这种情况它依然很公平的进行分发。 -
//消费者设置不公平分发.默认是0就是公平分发 channel.basicQos(1);
- 启动,生产者发送6条消息
- word3沉睡10s
- work4沉睡3s按照能者多劳的规则,所以word3要多干,这就是不公平算法
2.7.1 预取值
-
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费
者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此
缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。。这个时候就可以通过使用 basic.qos 方法设
置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量一旦数量达到配置的数量,
RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有
未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何
消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知
这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高
向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理
的消息的数量也会增加,从而增加了消费者的 RAM 消耗
耗(随机存取存储器)应该小心使用具有无限预处理
的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的
内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范
围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这
将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境
中。对于大多数应用来说,稍微高一点的值将是最佳的。 -
channel.basicQos();
-
里面的参数是0代表公平分发
-
里面的的参数是1代表的是不公平分发
-
里面的参数是>1是欲取值
-
在word4(睡眠3s)设置欲取值为2
-
在 word3(睡眠10s)设置欲取值为4
然后发送6条消息,word4拿到2条,word3拿到4条 -
欲取值在消息堆积的过程中看的更加直白,适用于消息堆积的太多处理
2.8 发布确认
- 之前的情况开启了持久化到磁盘上,但是有可能数据写入的时候或者写入了一半,消息就挂了
- 发布确认模式是消息写到磁盘上,将消息写入到磁盘中的时候写入完毕了Rabbitmq在告诉生产者我写完了
- 只有这样才能保证说消息保存完整
- 3.条加在一起才能保证数据不是丢失的
2.8.1 开启发布确认
- 发布确认默认是没有开启的,如果开启需要调用方法confirmSelect ,每当你想要发布确认都需要在channel上调用该方法
2.8.2 单个确认发布
- 这是一种简单的确认方式,它是一种同步确认的方式,也是发布一个消息之后,只有它被确认返回,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回
- 最大的缺点是:发布速度特别慢因为如果没有确认发布的消息就会阻塞后续消息的发布,这种方式最高提高每秒不超过数百条发布消息的吞吐量,当然对于某些程序来说已经足够了
- 以下是发布确认的单个发布确认,数据每持久化一次就确认一次
package com.rj.bd.five;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc
* @time 2022--11--15--21:03
*/
public class test {
public static final String QUER_NAMEA="ACK";
public static final Integer COUNT=1000;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
confirmSelect();
}
private static void confirmSelect() throws IOException, TimeoutException, InterruptedException {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//开启发布确认
channel.confirmSelect();
boolean durable=true; //队列开启持久化
//定义开始的时间
final long start = System.currentTimeMillis();
channel.queueDeclare(QUER_NAMEA,durable,false,false,null);
for (int i = 0; i <COUNT ; i++) {
String str="";
channel.basicPublish("", QUER_NAMEA, MessageProperties.PERSISTENT_BASIC, (str+i).getBytes());
final boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("发送成功");
}
}
//定义结束的时间
final long end = System.currentTimeMillis();
System.out.println("发送了"+COUNT+"条,所耗时间:"+(end-start)+"ms");
}
}
2.8.3 批量发布消息
- 相对于单个发布消息,批量发布消息要快,批量发布县发布一批消息然后一起确认可以极大的提高吞吐量
- 缺点就是当发生故障导致数据发生安全的时候,不知道是哪个消息出现问题了,我们必须将整个批量处理的保存在内存中,以记录重要的信息而重新发布消息,当然这种方法是同步的也一样阻塞消息的发布
- 发布的数量%100,每10次确认一次,批量发布消息消息所耗时间:121ms
package com.rj.bd.five;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc
* @time 2022--11--15--21:03
*/
public class test {
public static final String QUER_NAMEA="ACK";
public static final Integer COUNT=1000;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// confirmSelect(); 发送了1000条,所耗时间:927ms
confirmSelect();
}
private static void confirmSelect() throws IOException, TimeoutException, InterruptedException {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//开启发布确认
channel.confirmSelect();
boolean durable=true; //队列开启持久化
int count_size=100;
//定义开始的时间
final long start = System.currentTimeMillis();
channel.queueDeclare(QUER_NAMEA,durable,false,false,null);
for (int i = 0; i <COUNT ; i++) {
String str="";
channel.basicPublish("", QUER_NAMEA, MessageProperties.PERSISTENT_BASIC, (str+i).getBytes());
if (i%count_size==0){
//确认发布
final boolean flag = channel.waitForConfirms();
}
}
//定义结束的时间
final long end = System.currentTimeMillis();
System.out.println("发送了"+COUNT+"条,所耗时间:"+(end-start)+"ms");
}
}
2.8.3 异步发布消息
- 异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,
他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,
下面就让我们来详细讲解异步确认是怎么实现的。
- 配置发布确认的监听器,用过调用回调方法查看结果,因为是异步的所以执行效率块,因为有监听器还安全
private static void confirmSelectSync() throws IOException, TimeoutException, InterruptedException {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUER_NAMEA,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//定义开始的时间
final long start = System.currentTimeMillis();
//消息发送成功回调方法
/**
*1.消息序列号
* 2.是否批量
*/
ConfirmCallback callback=(var1, var3) ->{
System.out.println("消息确认发送完成"+var1);
};
//消息发送成失败回调方法
/**
*1.消息序列号
* 2.是否批量
*/
ConfirmCallback confirmCallback=(var1, var3) ->{
System.out.println("消息发送失败"+var1);
};
//监听
channel.addConfirmListener(callback, confirmCallback);
int i=0;
while (i<=1000) {
String str = "消息" + i;
channel.basicPublish("", QUER_NAMEA, null, str.getBytes());
i++;
//定义结束的时间
}
final long end = System.currentTimeMillis();
System.out.println("异步发送了"+COUNT+"条,所耗时间:"+(end-start)+"ms");
}
2.8.4 如何处理异步未确认消息
- 最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传
递。
//异步发送消息以及消息发送失败的处理
private static void confirmSelectSyncquee() throws IOException, TimeoutException, InterruptedException {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUER_NAMEA,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//定义开始的时间
final long start = System.currentTimeMillis();
/**
* 线程有序的一个哈希表
* 轻松的将序号与消息进行关联
* 轻松删除条目,只要给到序号
* 支持高并发(多线程)
*/
ConcurrentSkipListMap<Long,String> concurrentHashMap=new ConcurrentSkipListMap<Long, String>();
//消息发送成功回调方法
/**
*1.消息序列号
* 2.是否批量
*/
ConfirmCallback callback=(var1, var3) ->{
if (var3) {
ConcurrentNavigableMap<Long, String> longStringConcurrentNavigableMap = concurrentHashMap.headMap(var1,true);
longStringConcurrentNavigableMap.clear();
}else {
concurrentHashMap.remove(var1);
}
System.out.println("消息确认发送完成"+var1);
};
//消息发送成失败回调方法
/**
*1.消息序列号
* 2.是否批量
*/
ConfirmCallback confirmCallback=(var1, var3) ->{
String message = concurrentHashMap.get(var1);
System.out.println("发布的消息"+message+"未被确认,序列号"+concurrentHashMap);
};
//监听
channel.addConfirmListener(callback, confirmCallback);
for (Integer i = 0; i < COUNT; i++) {
String str = "消息" + i;
channel.basicPublish("", QUER_NAMEA, null, str.getBytes());
//存入的key是信道调取的下一次序号
concurrentHashMap.put(channel.getNextPublishSeqNo(),str);
// System.out.println("*------");
// System.out.println(concurrentHashMap.toString());
//定义结束的时间
}
final long end = System.currentTimeMillis();
System.out.println("异步发送了"+COUNT+"条,所耗时间:"+(end-start)+"ms");
}
三:Exchanges(交换机)
- 我们上面创建了一个工作队列,我们假设的是工作队列的背后,每个人物恰好交付给一个消费者,在这一部分中,我们将做一些完全不同的事情-我们将消息传递给多个消费者,这种模式称之为发布订阅模式
- 为了说明这种模式,构建一个简单的日志系统,它将由两个消息组成,第一个程序将发日志消息,第二个程序是消费者,我们会启动两个消费者,其中一个消费者将消息接受后将消息存储在磁盘中,另外一个将消息打印到公屏中,事实上第一个程序发出的消息将广播给所有的消费者
3.1 exchange的概念
- rabbitmq消息传递的核心思想是:生产者生产的消息不会直接发送到队列,实际上通常生产者都不知道自己创建了哪些队列
- 相反生产者只能将消息发送到Exchanges(交换机)上,交换机的工作内容十分简单,一方面它接受来自生产者的消息,另一方面将他们推入到队列中,交换机必须确切的知道他们应该处理哪些消息,是应该将这些消息放到特定的队列中还是说丢弃它们,这些都是由交换机的类型来决定
3.1.1 exchange的类型
直接( direct),主题(topic),标题(headres),扇出(fanout)
3.1.2 exchange的无名类型
之前没用到交换机,但是还是可以将数据发送到数据就是用的无名类型,也就是默认类型使用“”阿莱定义
第一个参数是交换机的名称,空串代表默认或者无交换机类型,消息能够发送到队列其实是由routingKey(bindingkey)绑定的key指定的,如果它存活
3.1.3 临时队列
- 之前使用的是具有特定名称的队列,队列的名称对于我们来说至关重要,-我们需要指定消费者去消费哪个队列的信息
- 每当我们连接到Rabbit时,我们都要创建一个全新的空队列,为此我们可以创建一个随机的队列,或者能让服务器给我们选择一个随机的队列就更好了,其次我们一旦断开了连接,队列将自动删除
- 创建临时队列的方式如下:
- String queryName=channel.queueDeclare().getQuery();
3.1.4 绑定(bindings)
- bindings的意思是exchage和queue之间的桥梁,它告诉我们exchange和哪个队列进行了绑定
- 下面这种图就是告诉了我们x与Q1和Q2进行了绑定
- 也可以在这里创建一个队列
- 在这里创建一个交换机
- 点击进去可以对队列进行捆绑
- 绑定刚刚创建的队列
- 当你进行发消息,就可以根据key --123寻找到对应的队列了
- 通过123关键词进行路由,将消息送到队列中,然后队列在送给消费者
- 可以通过RoutingKey进行区别对待,只给key是123的发消息,其他的不发送
3.2 Fanout(广播模式)
- Fanout的类型很简单,入名称一样,它是将接受到的消息广播给它所知道的所有队列中(发布订阅模式),系统之中有些默认的exchange类型
3.2.1 Fanout(消费者代码和生产者代码)
package com.rj.bd.six;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
/**
* @author LXY
* @desc 消费者1
* @time 2022--11--17--20:31
*/
public class ReceiveLog01 {
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
/**
* 第一个参数交换机的名称,第二个参数是交换机的类型(广播)
*/
channel.exchangeDeclare(EXCHINGE_NAME,"fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//给交换机和临时队列进行绑定
//第一个参数是队列名称,第二个参数是交换机
//第三个参数是绑定的key
channel.queueBind(queue,EXCHINGE_NAME,"");
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
//队列名称
//自动确认
//消息成功的回调
channel.basicConsume(queue,true,deliverCallback,callback);
System.out.println("启动成功");
}
}
- 消费者2
package com.rj.bd.six;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
/**
* @author LXY
* @desc 消费者2
* @time 2022--11--17--20:31
*/
public class ReceiveLog02 {
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
/**
* 声明交换机
* 第一个参数交换机的名称,第二个参数是交换机的类型(广播)
*/
channel.exchangeDeclare(EXCHINGE_NAME,"fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//给交换机和临时队列进行绑定
//第一个参数是队列名称,第二个参数是交换机
//第三个参数是绑定的key
channel.queueBind(queue,EXCHINGE_NAME,"");
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
//绑定交换机
//自动确认
//消息成功的回调
channel.basicConsume(queue,true,deliverCallback,callback);
System.out.println("启动成功");
}
}
- 生产者代码
package com.rj.bd.six;
import com.rabbitmq.client.Channel;
import com.rj.bd.utils.RabbitMqUtils;
import java.util.Scanner;
/**
* @author LXY
* @desc 生产者 发消息给交换机
* @time 2022--11--17--20:41
*/
public class EmitLog {
//交换机的名称
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
// channel.exchangeDeclare(EXCHINGE_NAME,"faout");
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
System.out.println("请输入发送的消息:");
String str=scanner.next();
/**
* 1.第一个参数表示发送到哪个交换机
* 2.路由的key是哪个,队列的名称
* 3.其他的参数
* 4.发送的消息
*/
channel.basicPublish(EXCHINGE_NAME,"",null,str.getBytes());
}
}
}
- 先启动消费者,然后再启动生产者,就可以实现广播的效果
- 原理,消费者通过log的交换机绑定的临时队列
- 生产者只需要将消息发送到交换机即可,交换机会根据绑定的key去寻找指定的队列
- 因为绑定的交换机类型是发布订阅模式的,所以不管key相不相同都会进行发布订阅
3.3 direct 直接交换机
- 上一节中的我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希
望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志
消息避免浪费磁盘空间。Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的
广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的
routingKey 队列中去
3.3.1 direct 多重绑定
- 消费者1代码实现
package com.rj.bd.seven;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
/**
* @author LXY
* @desc
* @time 2022--11--17--21:17
*/
public class directlog1 {
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
/**
* 第一个参数交换机的名称,第二个参数是交换机的类型(路由)
*/
channel.exchangeDeclare(EXCHINGE_NAME, BuiltinExchangeType.DIRECT);
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//给交换机和临时队列进行绑定
//第一个参数是队列名称,第二个参数是交换机
//第三个参数是绑定的key
channel.queueBind(queue,EXCHINGE_NAME,"info"); //只接受info和other的消息
channel.queueBind(queue,EXCHINGE_NAME,"other");
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("directlog1:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
//队列名称
//自动确认
//消息成功的回调
channel.basicConsume(queue,true,deliverCallback,callback);
System.out.println("启动成功");
}
}
- 消费者2代码实现
package com.rj.bd.seven;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
/**
* @author LXY
* @desc
* @time 2022--11--17--21:17
*/
public class directlog2 {
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
/**
* 第一个参数交换机的名称,第二个参数是交换机的类型(路由)
*/
channel.exchangeDeclare(EXCHINGE_NAME, BuiltinExchangeType.DIRECT);
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//给交换机和临时队列进行绑定
//第一个参数是队列名称,第二个参数是交换机
//第三个参数是绑定的key
channel.queueBind(queue,EXCHINGE_NAME,"erro"); //只接受erro的消息
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("directlog1:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
//队列名称
//自动确认
//消息成功的回调
channel.basicConsume(queue,true,deliverCallback,callback);
System.out.println("启动成功");
}
}
- 生产者代码实现
package com.rj.bd.seven;
import com.rabbitmq.client.Channel;
import com.rj.bd.utils.RabbitMqUtils;
import java.util.Scanner;
/**
* @author LXY
* @desc 生产者 发消息给交换机
* @time 2022--11--17--20:41
*/
public class Direct_EmitLog {
//交换机的名称
private static final String EXCHINGE_NAME="logs";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
System.out.println("请输入发送的消息:");
String str=scanner.next();
/**
* 1.第一个参数表示发送到哪个交换机
* 2.路由的key是哪个,队列的名称
* 3.其他的参数
* 4.发送的消息
*/
if (str.contains("erro")){
channel.basicPublish(EXCHINGE_NAME,"erro",null,str.getBytes());
}else if (str.contains("info")){
channel.basicPublish(EXCHINGE_NAME,"info",null,str.getBytes());
}else {
channel.basicPublish(EXCHINGE_NAME,"other",null,str.getBytes());
}
}
}
}
-
多冲绑定,并且交换机是路由模式,通过交换机绑定的key来发送不同的数据
-
生成者 发送数据,判断数据是否包含,然后通过绑定的key对数据进行发送,所以绑定的不同的key会接受到不同的消息
-
消费者1 接受info 级别的数据和其他的数据
-
消费者2 接受erro的数据,
-
运行截图
3.4 Topice
- Topic比Fanout和direct交换机更加的完美
- direct很局限性,比如有info.base和info.advantage,某个队列只想要Info.base的信息,那么direct就办不到了,只能使用Topice类型
3.4.1 Topice的要求
- 发送的类型是topic,交换机消息的routing_key不能随意写,必须满足一定要求,它必须是一个单词的列表以.分开这些单词可以是任意的单词,“stock.usd.nyse”,“nyse.vmw”,“quick.orang.rabbit”等当然这些单词不能超多255个字节
- *可以代替一个单词
- #可以代替0或者多个单词
3.4.2 Topice的匹配
- Q1绑定的是
- 中间带orang,带3个字符的字符串,(星号.orang. 星号)
- Q2绑定的是
- 最后一个单词是rabbit的3个字符(星号.星号.rabbit)
- 第一个单词是lazy后面是多个单词(lazy.#)
3.4.3 代实现
- 消费者1
package com.rj.bd.Topic;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 消费者1
* @time 2022--11--19--14:51
*/
public class TopicCun01 {
private static final String EXCHINGE_NAME="Topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
String queue = channel.queueDeclare().getQueue(); //创建临时队列
channel.exchangeDeclare(EXCHINGE_NAME, BuiltinExchangeType.TOPIC); //绑定交换机
channel.queueBind(queue,EXCHINGE_NAME,"*.range.*"); //队列交换机相互绑定
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
channel.basicConsume(queue,true,deliverCallback,callback);
}
}
- 消费者2
package com.rj.bd.Topic;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 消费者1
* @time 2022--11--19--14:51
*/
public class TopicCun02 {
private static final String EXCHINGE_NAME="Topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
String queue = channel.queueDeclare().getQueue(); //创建临时队列
channel.exchangeDeclare(EXCHINGE_NAME, BuiltinExchangeType.TOPIC); //绑定交换机
channel.queueBind(queue,EXCHINGE_NAME,"*.*.rabbit"); //队列交换机相互绑定
channel.queueBind(queue,EXCHINGE_NAME,"lazy.#"); //队列交换机相互绑定
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
channel.basicConsume(queue,true,deliverCallback,callback);
}
}
- 生产者
package com.rj.bd.Topic;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 生产者
* @time 2022--11--19--14:51
*/
public class Topicpor {
private static final String EXCHINGE_NAME="Topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner=new Scanner(System.in);
System.out.println("请输入");
while (scanner.hasNext()){
String next = scanner.next();
channel.basicPublish(EXCHINGE_NAME,"lazy.range.s",null,next.getBytes());
}
}
}
消费者1绑定了一个交换机,key是*.range.*
消费者2绑定了两个交换机,key是*.*.rabbit lazy.#
生产者发送交换机饿key是lazy.range.s,两个都匹配
四:死信队列
- 死信,就是无法被消费的消息,一般来说producer将消息投递到broker或者直接到queue里,consumer从query中取出消息进行消费,但由于某些特定的原因导致queue中信息无法被消费 这样的消息如果没有后续的处理,就变成了死信,有死信就自然有死信队列
- 业务场景:为了保证业务的订单的消息数据不会丢失,需要使用到rabbitmq消息队列中的死信机制,当消息发生异常的时候将消息投入到死信队列中,还有就是说,用户在商城下单成功,并点击支付在指定时间内未支付的时候自动失效
4.1死信的来源
- 消息的ttl过期
- 队列达到的最大长度(消息满了无法在将数据添加到mq中)
- 消息被拒绝(basic.reject或者basic.nack)并且requeue=false
4.1死信队列代码实现
-
执行流程
-
消费者有死信交换机-死信队列
-
还有普通交换机和普通队列
-
在普通的队列中添加map参数
-
设置死信转发到哪个交换机,和交换机的key。这样消息成了死信,然后就可以通过map中绑定的死信地址去将消息发送给对应的交换机转发到对应的死信队列中
-
先启动消费者,然后关闭,在启动生产者执行10条数据,这样消费者宕机了10s中没有收到消息就成了死信消息就进入了死信队列和交换机
-
消费者代码
package com.rj.bd.deld;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 死信队列消费者
* @time 2022--11--19--16:14
*/
public class deldCum {
//普通交换机
private static final String NORMAL_EXCHINGE="NORMAL_logs";
//死信交换机
private static final String DELD_EXCHINGE="DELD_logs";
//普通队列
private static final String NORMAL_QUEUE="NORMAL";
//死信队列
private static final String DELE_QUEUE="DELD";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Channel channel = RabbitMqUtils.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHINGE, BuiltinExchangeType.DIRECT);
//声明死信交换机
channel.exchangeDeclare(DELD_EXCHINGE, BuiltinExchangeType.DIRECT);
//普通队列的参数设置
Map<String, Object> map=new HashMap<>();
//声明死信交换机
map.put("x-dead-letter-exchange",DELD_EXCHINGE);
//正常队列设置死信 routing-key 参数 key 是固定值
map.put("x-dead-letter-routing-key", "dele");
//声明普通队列
channel.queueDeclare(NORMAL_QUEUE,false,false,false,map);
//声明死信
channel.queueDeclare(DELE_QUEUE,false,false,false,null);
//交换机和队列相互绑定
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHINGE,"normal");
//交换机和队列相互绑定
channel.queueBind(DELE_QUEUE,DELD_EXCHINGE,"dele");
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,callback);
}
}
- 生产者
package com.rj.bd.deld;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc
* @time 2022--11--19--16:28
*/
public class deldEmit {
//普通交换机
private static final String NORMAL_EXCHINGE="NORMAL_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//设置队列的过期时间
AMQP.BasicProperties basicProperties=new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 0; i < 10; i++) {
String str="这是第"+i+"消息";
channel.basicPublish(NORMAL_EXCHINGE,"normal",basicProperties,str.getBytes());
}
}
}
- 死信消费者代码
package com.rj.bd.deld;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rj.bd.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author LXY
* @desc 死信队列消费者
* @time 2022--11--19--16:14
*/
public class deldCum2 {
//死信交换机
private static final String DELD_EXCHINGE="DELD_logs";
//死信队列
private static final String DELE_QUEUE="DELD";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(DELD_EXCHINGE, BuiltinExchangeType.DIRECT);
// channel.queueDeclare(DELE_QUEUE, false, false, false, null);
channel.queueBind(DELE_QUEUE, DELD_EXCHINGE, "dele");
//成功接收消息
DeliverCallback deliverCallback=(str , message) ->{
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody()));
};
//接受消息失败的回调
CancelCallback callback=(str) ->{
System.out.println("执行失败:"+str);
};
channel.basicConsume(DELE_QUEUE,true,deliverCallback,callback);
}
}
4.1.1 死信队列消息最大长度
- 在消费者1中
- 生产者
- 生产者
-然后将队列删除,重启消费者1,然后关掉。然后启动生产者发送消息
- 6条是普通队列,剩下的是死信队列
4.1.2 死信队列消息被拒
- 在消费者1的成功回调消息中将第八条消息拒绝买房到死信队列中
- 设置手动应答
if (messg.equals("这是第7消息")){
//如果是第八条消息我们将消息拒绝并且不赛队列,然后 就变成了死信队列
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else {
System.out.println("ReceiveLog01接收到的消息:" + messg);
//否则就接受消息,不批量应答
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
- 消息为8的会放到死信队列,这就是消息被拒绝
五:延迟队列
- 延迟队列,队列的内部是有序的,最重要的特性就会体现在他的延迟属性上,延迟队列中的元素是希望到了指定事件之后或者之前进行处理,简单的来说,延迟队列就是用来存放,需要在指定的时间内被处理的元素队列
5.1 延迟队列的使用场景
- 订单在10分钟未支付则取消
- 新创建的店铺,在10分钟内没有上传过商品,则定时发送消息
- 新用户注册后,3天未上传新商品则进行短信提醒
5.2 RabbitMQ整合Springboot项目
pom文件
<?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>
<groupId>org.example</groupId>
<artifactId>huo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- Spring Boot 启动父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version> <!-- 1.5.1-->
</parent>
<dependencies>
<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.4.3.1</version>
</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-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--S Lombok插件依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--swagger2依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger2-ui依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--E Lombok插件依赖-->
<!-- MySQL 连接驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
</dependencies>
</project>
修改配置文件
spring.rabbitmq.host=192.168.116.132
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
在config添加swagger
package com.rj.bd.Config;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author LXY
* @desc swagger2配置文件
* @time 2022-08-13 13:40
*/
@Configuration//配置类
@EnableSwagger2 //swagger注解
public class Swagger2Config {
@Bean
public Docket getdocker(){
//指明SWAGGER_2的版本使用spring容器进行管理
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(getApoInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.apis(RequestHandlerSelectors.basePackage("com.rj.bd.Web")) //配置扫描路径
.paths(
//可以再里面写多个
PathSelectors.any()
)
.build();
return docket;
}
public ApiInfo getApoInfo(){
return new ApiInfoBuilder()
.title("测试项目")
.version("1.0")
.description("测试项目接口文档")
.build();
}
}
5.2.1 延迟队列代码架构图
5.2.2 Springboot代码实现 ttl
package com.rj.bd.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;
/**
* @author LXY
* @desc
* @time 2022--11--19--18:14
*/
@Configuration
public class TtlQueueConfig {
//一个普通交换机,两个普通队列
//一个死信交换机,一个死信队列
//普通交换机
private static final String NONE_ex="NONE_exs";
//普通队列
private static final String NONE_A="none_a";
//普通队列
private static final String NONE_B="none_b";
//死信交换机
private static final String DELE_ex="DELE_exs";
//死信队列
private static final String NONE_C="none_c";
//声明普通交换机
@Bean("getNONE_ex")
public DirectExchange getNONE_ex(){
return new DirectExchange(NONE_ex);
}
//声明普通队列A
@Bean("geNONE_A")
public Queue geNONE_A(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DELE_ex);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "Bing_C");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(NONE_A).withArguments(args).build();
}
//声明普通队列B
@Bean("getNONE_B")
public Queue getNONE_B(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DELE_ex);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "Bing_C");
//声明队列的 TTL
args.put("x-message-ttl", 4000);
return QueueBuilder.durable(NONE_B).withArguments(args).build();
}
//队列A绑定普通交换机
@Bean("BindingqueueA")
public Binding BindingqueueA(@Qualifier("geNONE_A") Queue queue,@Qualifier("getNONE_ex") DirectExchange none_exchage){
return BindingBuilder.bind(queue).to(none_exchage).with("Bing_A");
}
//队列A绑定普通交换机
@Bean("BindingqueueB")
public Binding BindingqueueB(@Qualifier("getNONE_B") Queue queue,@Qualifier("getNONE_ex") DirectExchange none_exchage){
return BindingBuilder.bind(queue).to(none_exchage).with("Bing_B");
}
//声明死信交换机
@Bean("getDELE_ex")
public DirectExchange getDELE_ex(){
return new DirectExchange(DELE_ex);
}
//声明死信队列C
@Bean("getDELE_ex_C")
public Queue getDELE_ex_C(){
return new Queue(NONE_C);
}
//信队列C绑定死信交换机
@Bean("BindingqueueC")
public Binding BindingqueueC(@Qualifier("getDELE_ex_C") Queue queue,@Qualifier("getDELE_ex") DirectExchange none_exchage){
return BindingBuilder.bind(queue).to(none_exchage).with("Bing_C");
}
}
- 死信消费者代码
package com.rj.bd.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.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--19--18:35
*/
@Slf4j
@Component
public class DeldCunsumer {
@RabbitListener(queues = "none_c")
// @RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
- 客户端发送数据 运用rabbitTemplate 模板
@RestController
@RequestMapping("api/")
@Api( tags = { "RoleController" },description = "人员信息")
@Slf4j
public class RoleController {
@Autowired
RabbitTemplate rabbitTemplate ;
// //rabbitmq的模板
//
@ApiOperation("发送消息")
@RequestMapping(value = "message/{mes}",method = RequestMethod.GET)
public void messsend(@PathVariable String mes) {
log.info(new Date().toString()+"发送了一条消息内容为:"+mes+",延迟10s");
log.info(new Date().toString()+"发送了一条消息内容为:"+mes+",延迟4s");
//
rabbitTemplate.convertAndSend("NONE_exs","Bing_A",mes);
rabbitTemplate.convertAndSend("NONE_exs","Bing_B",mes);
}
}
5.2.3 Springboot代码实现 ttl 优化(自定义延迟时间)
- 在创建一个普通队列进行绑定
MessagePostProcessor messagePostProcessor=(message) ->{
message.getMessageProperties().setExpiration("10000");
return message;
};
5.2.3 Springboot代码实现 ttl 优化(延迟队列排队的问题)
- 发送一条延迟10s的信息,在发送延迟1s的信息,理论上应该是延迟1s的信息先到,但是延迟1s会按照第一条也就是延迟10s为准,你的第一条信息不发出来,第二条信息不会出来的,产生了一个延迟队列排队的问题
因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,
如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
5.2.4 安装插件解决问题
- 在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
- 第一个命令是查看rabbitmq安装的位置,然后将插件拷贝搭配pligins下
- 通过cp 拷贝过去
- 在当前目录下执行安装命令
- 安装不上的就sudo 一下
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 安装完成执行命令重启
systemctl restart rabbitmq-server
- 有这个就代表安装好了
- 现在会发现安装了插件是基于交换机的
5.2.5 基于插件的代码时间(解决延迟排队)
- 通过插件解决先进先出得排队问题
- config文件
package com.rj.bd.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;
/**
* @author LXY
* @desc
* @time 2022--11--19--22:41
*/
@Configuration
public class DelayedConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机
@Bean
public CustomExchange getCustomExchange(){
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");//自定义交换机的类型
//第一个参数,交换机的名称
//交换机的类型
//是否持久化
//是否自动删除
//其他参数
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
args);
}
//交换机和队列相绑定
@Bean
public Binding BindingDelayedA(@Qualifier("delayedQueue")Queue queue,@Qualifier("getCustomExchange")CustomExchange cum){
return BindingBuilder.bind(queue).to(cum).with(DELAYED_ROUTING_KEY).noargs();
}
}
- 生产者代码
@ApiOperation("发送消息")
@RequestMapping(value = "getmesssend/{mes}/{ttl}",method = RequestMethod.GET)
public void getmesssend(@PathVariable("mes") String mes,@PathVariable("ttl") Integer ttl) {
log.info(new Date().toString()+"发送了一条消息内容为:"+mes+",延迟"+ttl);
MessagePostProcessor messagePostProcessor=(message) ->{
message.getMessageProperties().setDelay(ttl);
return message;
};
rabbitTemplate.convertAndSend(DelayedConfig.DELAYED_EXCHANGE_NAME,DelayedConfig.DELAYED_ROUTING_KEY,mes,messagePostProcessor);
}
- 消费者代码
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.Config.DelayedConfig;
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.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--19--22:52
*/
@Component
@Slf4j
public class DelayedConsumer {
@RabbitListener(queues = DelayedConfig.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
- 先发送一个10s延迟的,在发送一个3s延迟的,如果是3s延迟的先走就对了
六:发布确认(高级 )
- 在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,
导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?
特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:
6.1 代码架构
6.2 代码实现
- config文件
package com.rj.bd.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;
/**
* @author LXY
* @desc 发布确认高级
* @time 2022--11--19--22:41
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME = "Confirm.exchange";
//队列
public static final String DELAYED_QUEUE_NAME = "Confirm.queue";
//DELAYED_ROUTING_KEY
public static final String DELAYED_ROUTING_KEY = "Confirm.routingkey";
@Bean
public Queue dConfirmdQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机
@Bean
public DirectExchange confirmExchange(){
return new DirectExchange(DELAYED_EXCHANGE_NAME);
}
//交换机和队列相绑定
@Bean
public Binding BindingConfirm(@Qualifier("dConfirmdQueue")Queue queue,@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY);
}
}
- 生产者
@ApiOperation("发送消息")
@RequestMapping(value = "getmesssend/{mes}",method = RequestMethod.GET)
public void getConfirmmessage(@PathVariable("mes") String mes) {
log.info(new Date().toString()+"发送了一条消息内容为:"+mes);
rabbitTemplate.convertAndSend(DelayedConfig.DELAYED_EXCHANGE_NAME,DelayedConfig.DELAYED_ROUTING_KEY,mes);
}
- 消费者
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.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;
import java.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--19--23:32
*/
@Slf4j
@Component
public class ConfirmCunsumer {
@RabbitListener(queues= ConfirmConfig.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
- 这个是交换机没出故障的时候
6.2.1 回调接口(实现交换机确认)
- 在上面的代码上新增MyRabbitMq类(回调接口类)
package com.rj.bd.Web;
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;
/**
* @author LXY
* @desc
* @time 2022--11--20--0:00
*/
@Component
@Slf4j
//继承的是RabbitTemplate的内部的一个接口
public class MyRabbitMq implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//初始化方法需要将本类替换掉 RabbitTemplate.ConfirmCallback的方法
@PostConstruct //注解的意思是等其他注解执行完在执行
public void init(){
//将自己替换进去
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机回调之后的方法
* 1. 发消息,交换机收到了回调
* 1.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是true
* 1.3交换机收到消息为null
* 交换机失败了回调的方法
* 2.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是 false
* 2.3交换机收到消息为 失败的原因
* @param correlationData
* @param b
* @param s
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
//true代表交换机成功接受到下次
if (b){
log.info("交换机成功接受到id为{}的消息"+id);
}else {
log.info("交换机接受失败id为{}的原因,出错原因是{}"+id,s);
}
}
}
- 生产者新增
- 在application.properties 增加
spring.rabbitmq.publisher-confirm-type=correlated
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
⚫ NONE
禁用发布确认模式,是默认值
⚫ CORRELATED
发布消息成功到交换器后会触发回调方法
⚫ SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法
等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
-
交换机成功运行效果图
-
交换机运行失败
6.3回退消息
- 如果在交换机没问题,但是在交换机发送消息到队列的这一段时间发生了问题,此时我们是不知道的
6.3.1Mandatory 参数
- 在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何
让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参
数可以在当消息传递过程中不可达目的地时将消息返回给生产者。 - 1.修改配置文件
spring.rabbitmq.publisher-returns=true
- 在自定义的MyRabbitMq类中,实现消息回退接口
- 注入
- 冲写办法
- 完整的MyRabbitMq代码
package com.rj.bd.Web;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
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;
/**
* @author LXY
* @desc
* @time 2022--11--20--0:00
*/
@Component
@Slf4j
//继承的是RabbitTemplate的内部的一个接口
public class MyRabbitMq implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
//初始化方法需要将本类替换掉 RabbitTemplate.ConfirmCallback的方法
@PostConstruct //注解的意思是等其他注解执行完在执行
public void init(){
//将自己替换进去
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机回调之后的方法
* 1. 发消息,交换机收到了回调
* 1.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是true
* 1.3交换机收到消息为null
* 交换机失败了回调的方法
* 2.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是 false
* 2.3交换机收到消息为 失败的原因
* @param correlationData
* @param b
* @param s
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
//true代表交换机成功接受到下次
if (b){
log.info("交换机成功接受到id为{}的消息"+id);
}else {
log.info("交换机接受失败id为{}的原因,出错原因是{}"+id,s);
}
}
@Override
public void returnedMessage(Message message, int i, String replyText, String exchange, String routingkey) {
log.info("消息发送失败:失败的消息是{},失败的路由是{},失败的原因是{},失败的路由Kry是{}",
new String(message.getBody()),exchange,replyText,routingkey);
}
}
- 在生产者这里发送一个不存在的KEY
- 效果图
6.4 备份交换机
-
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息
无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然
后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者
所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增
加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的
复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些
处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份
交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,
就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由
备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑
定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都
进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。 -
我们可以让队列通过备份交换机的方式,也就是俗称故障转移,当路由向队列发送失败的话,直接转移到路由交换机上
- -
修改config
新增备份交换机
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 backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
@Bean
public Queue backupqueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
@Bean
public Queue backupwaring(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
@Bean
public Binding BindingConfirmbackup(@Qualifier("backupqueue")Queue queue,@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding BindingConfirmwar(@Qualifier("backupwaring")Queue queue,@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
- 需要在普通的交换机进行一个指向,指向备份交换机
- WARNINGCunsumer类
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.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;
import java.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--20--1:04
*/
@Component
@Slf4j
public class WARNINGCunsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveD_warning(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("报警队列:当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
- 效果展示
6.5 消息的发布确认完整版代码(交换机的备份,回调,消息的回调和报警)
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先
级高,经过上面结果显示答案是备份交换机优先级高。
- config配置文件(配置了普通交换机,备份交换机)
package com.rj.bd.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;
/**
* @author LXY
* @desc 发布确认高级
* @time 2022--11--19--22:41
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME = "Confirm.exchange";
//队列
public static final String DELAYED_QUEUE_NAME = "Confirm.queue";
//DELAYED_ROUTING_KEY
public static final String DELAYED_ROUTING_KEY = "Confirm.routingkey";
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 Queue dConfirmdQueue() {
return QueueBuilder.durable(DELAYED_QUEUE_NAME).build();
}
//自定义交换机
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(DELAYED_EXCHANGE_NAME).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
//交换机和队列相绑定
@Bean
public Binding BindingConfirm(@Qualifier("dConfirmdQueue")Queue queue,@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY);
}
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
@Bean
public Queue backupqueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
@Bean
public Queue backupwaring(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
@Bean
public Binding BindingConfirmbackup(@Qualifier("backupqueue")Queue queue,@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding BindingConfirmwar(@Qualifier("backupwaring")Queue queue,@Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
- application.properties
spring.rabbitmq.publisher-confirm-type=correlated
#开启退消息
spring.rabbitmq.publisher-returns=true
- 生产者
@ApiOperation("发送消息")
@RequestMapping(value = "getmesssend/{mes}",method = RequestMethod.GET)
public void getConfirmmessage(@PathVariable("mes") String mes) {
log.info(new Date().toString()+"发送了一条消息内容为:"+mes);
CorrelationData correlationData=new CorrelationData("001");
rabbitTemplate.convertAndSend(ConfirmConfig.DELAYED_EXCHANGE_NAME,ConfirmConfig.DELAYED_ROUTING_KEY+"2",mes,correlationData);
}
- 自定义回调类(MyRabbitMq)
package com.rj.bd.Web;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
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;
/**
* @author LXY
* @desc
* @time 2022--11--20--0:00
*/
@Component
@Slf4j
//继承的是RabbitTemplate的内部的一个接口
public class MyRabbitMq implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
//初始化方法需要将本类替换掉 RabbitTemplate.ConfirmCallback的方法
@PostConstruct //注解的意思是等其他注解执行完在执行
public void init(){
//将自己替换进去
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机回调之后的方法
* 1. 发消息,交换机收到了回调
* 1.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是true
* 1.3交换机收到消息为null
* 交换机失败了回调的方法
* 2.1 保存消息的id以及其他的信息
* 1.2 交换机收到消息是 false
* 2.3交换机收到消息为 失败的原因
* @param correlationData
* @param b
* @param s
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
//true代表交换机成功接受到下次
if (b){
log.info("交换机成功接受到id为{}的消息"+id);
}else {
log.info("交换机接受失败id为{}的原因,出错原因是{}"+id,s);
}
}
@Override
public void returnedMessage(Message message, int i, String replyText, String exchange, String routingkey) {
log.info("消息发送失败:失败的消息是{},失败的路由是{},失败的原因是{},失败的路由Kry是{}",
new String(message.getBody()),exchange,replyText,routingkey);
}
}
- 普通队列接受消息类
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.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;
import java.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--19--23:32
*/
@Slf4j
@Component
public class ConfirmCunsumer {
@RabbitListener(queues= ConfirmConfig.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
//
// @RabbitListener(queues = ConfirmConfig.BACKUP_QUEUE_NAME)
// public void receiveD_backup(Message message, Channel channel) throws IOException {
// String msg = new String(message.getBody());
// log.info("备份队列:当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
// }
}
- 队列监控预警
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.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;
import java.io.IOException;
import java.util.Date;
/**
* @author LXY
* @desc
* @time 2022--11--19--23:32
*/
@Slf4j
@Component
public class ConfirmCunsumer {
@RabbitListener(queues= ConfirmConfig.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
//
// @RabbitListener(queues = ConfirmConfig.BACKUP_QUEUE_NAME)
// public void receiveD_backup(Message message, Channel channel) throws IOException {
// String msg = new String(message.getBody());
// log.info("备份队列:当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
// }
}
七:Rabbitmq的其他知识点
7.1 幂等性
概念:
- 用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,
此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱
了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误
立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等
7.1.1 重复消费
- 消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,
故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但
实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
7.1.2 解决思路
- MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费
者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消
息时用该 id 先判断该消息是否已消费过。
7.1.3 消费端的幂等性保障
- 在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,
这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.
唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现
7.1.4 唯一 ID+指纹码机制
- 指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存
在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数
据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
7.1.5 Redis的原子性
- 利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
7.2 优先级队列
- 在系统中有订单催付的场景,我们的客户在天猫下的订单,淘宝会及时的将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条消息提醒,很简单的一个功能,但是对于商家来说,肯定要分大客户和小客户对吧,比如像苹果,小米这样的大商家,一年起码能给我们创造很大的利润,所以,理所应当,它们的订单必须得到优先处理,而我们的后端系统采用的是redis来存放的定时轮训,大家之后redis只能用list做一个简简单单的队列,并不能实现一个优先级的场景,所以订单量大了之后采用Rabbitmq进行改造和优化,如果发现是大客户的订单,给一个现对于比较高的优先级,否则就是默认优先级
7.3 惰性队列
- RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消
费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持
更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致
使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,
这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留
一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,
但是效果始终不太理想,尤其是在消息量特别大的时候
- 惰性队列的消费是很慢的,一一般是不采用的
- 消费者宕机了,mq中堆积了很多的消息,这样就可以采用惰性队列,将消息存在磁盘上,就不会浪费内存了
7.3.1 两种模式
- 队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy
模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过
Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。
如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
7.3.2 内存开销对比
- 在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅
占用 1.5MB
八:Rabbitmq的集群
8.1 使用集群的原因
- 最开始我们介绍了如何安装及运行 RabbitMQ 服务,不过这些是单机版的,无法满足目前真实应用的
要求。如果 RabbitMQ 服务器遇到内存崩溃、机器掉电或者主板故障等情况,该怎么办?单台 RabbitMQ
服务器可以满足每秒 1000 条消息的吞吐量,那么如果应用需要 RabbitMQ 服务满足每秒 10 万条消息的吞
吐量呢?购买昂贵的服务器来增强单机 RabbitMQ 务的性能显得捉襟见肘,搭建一个 RabbitMQ 集群才是
解决实际问题的关键
8.2 集群搭建
-
1.先确保自己有3台虚拟机并且3台虚拟机的rabbitmq都可以启动
-
我的是192.168.116.132 -133-134这三台虚拟机
-
2.执行命令: vim /etc/hostname
-
确保自己三天虚拟机的hostname不同,因为我这个是上次搭建hadoop的虚拟机
-
对应的132—>hapool102
-
133–>hapool103
-
134 -->hapool104
-3.执行vim /etc/hosts -
让3台虚拟机可以通过其他虚拟机的ip(192.168.11. xxx)识别到对应的主机名称(hapoolxxx)
-
我这里上次配置hadoop的时候已经配置好了
-
4.确保各个节点的cokie是相同的值
-
在主虚拟机上执行
-
scp /var/lib/rabbitmq/.erlang.cookie root@hapool103:/var/lib/rabbitmq/.erlang.cookie
-
scp /var/lib/rabbitmq/.erlang.cookie root@hapool104:/var/lib/rabbitmq/.erlang.cookie
-
5.重启每台虚拟机
rabbitmq-server -detached -
6.在第二个节点上执行(加入到第一个节点为集群)
cd
/sbin/service rabbitmq-server stop
/sbin/service rabbitmq-server start
# (rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务) 直接执行的话有的时候会有bug,所以重启一下在执下面这句话
rabbitmqctl stop_app
#执行重置
rabbitmqctl reset
# 将2号节点加入到1号节点上
rabbitmqctl join_cluster rabbit@hapool102
#(只启动应用服务)
rabbitmqctl start_app
- 7.在第三个节点上执行(加入到第二个节点为集群)
cd
/sbin/service rabbitmq-server stop
/sbin/service rabbitmq-server start
# (rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务) 直接执行的话有的时候会有bug,所以重启一下在执下面这句话
rabbitmqctl stop_app
#执行重置
rabbitmqctl reset
# 将2号节点加入到1号节点上
rabbitmqctl join_cluster rabbit@hapool103
#(只启动应用服务)
rabbitmqctl start_app
- 8.查看集群状态
rabbitmqctl cluster_status
- 集群搭建成功
8.3 集群解除
- 解除节点.解除集群节点(hapool103和 hadoop104 机器分别执行)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@hapool102(hadoop3 机器上执行)
8.4 镜像队列
-
我们创建的队列,不管在哪台服务器上创建的队列,队列只属于第一台服务器的队列,也就是hadoop102,其他的hapool3和hapool4是没有的
-
一旦节点1宕机了,消息就丢失了
-
在102节点上执行
rabbitmqctl stop_app
让服务器宕机
- 可以看到队列为宕机状态
- 执行 rabbitmqctl start_app 启动102节点
8.4.1 镜像队列概念
- 如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并
且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,
但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在
一个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息己经存入磁盘,
尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中
的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。 - 可以选择佩芬1分或者备份全部
8.4.2 镜像队列的搭建
- 在任何一个节点上
- 添加策略
- 然后创建一个队列
- 执行 rabbitmqctl stop_app让主机点宕机
- 就会发现原来数据是在102和103上,我们的102宕机了数据就会存储在103和104上
- 这样即使有一台机器存活数据就不会丢失
8.5 高可用负载均衡
- 我们只能连接一个mq。如果这个mq宕机了,生产者还在连接这个mq,肯定是连接不上的,另外两个mq还是可以能工作的,这个时候就得让他连接二号机或者三号机,但是我们的程序是不知道有二号机或者三号机的存在的
- 因为已经宕机了,所以连接不到
8.6 . Haproxy+Keepalive 实现高可用负载均衡
- HAProxy 提供高可用性、负载均衡及基于 TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并
且可靠的一种解决方案,包括 Twitter,Reddit,StackOverflow,GitHub 在内的多家知名互联网公司在使用。
HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。
扩展 nginx,lvs,haproxy 之间的区别: http://www.ha97.com/5646.html - 我们是直接请求在左边的主机上,然后去访问这三个集群,右边的充当备用,Keepalive 会发现你宕机了 ,在的话就不管了,宕机的话会将ip票到右边的备用机上,备用机也会询问你还在吗,不在的话直接将请打在备用机上,然后右边的备用机会去继续路由这三台集群
- 还可以实现负载均衡,100w数据可以两个机器平摊数据,实现高可用
九:Federation Exchang(联邦交换机)
十:Springboot实现Topic交换机
- top
package com.rj.bd.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;
/**
* @author LXY
* @desc
* @time 2022--11--20--17:44
*/
@Configuration
public class TopConfig {
//Topice交换机
public final static String TOP_EXCHANGE="topExchange";
//Topice交换机队列1
public final static String QUEUE_NAME1="queue_one";
//Topice交换机队列2
public final static String QUEUE_NAME2="queue_two";
//声明交换机
@Bean
public TopicExchange getTOP_EXCHANGE(){
return new TopicExchange(TOP_EXCHANGE);
}
//声明交换机1
@Bean
public Queue getQUEUE_NAME1(){
return QueueBuilder.durable(QUEUE_NAME1).build();
}
//声明交换机2
@Bean
public Queue getQUEUE_NAME2(){
return QueueBuilder.durable(QUEUE_NAME2).build();
}
//第一个交换机绑定
@Bean
public Binding getTOP_EXCHANGE_QUEUQ_ONE(@Qualifier("getQUEUE_NAME1")Queue queue, @Qualifier("getTOP_EXCHANGE") TopicExchange topicExchange){
return BindingBuilder.bind(queue).to(topicExchange).with("#.to");
}
//第二个交换机绑定
@Bean
public Binding getTOP_EXCHANGE_QUEUQ_TWO(@Qualifier("getQUEUE_NAME2")Queue queue, @Qualifier("getTOP_EXCHANGE") TopicExchange topicExchange){
return BindingBuilder.bind(queue).to(topicExchange).with("*.to.*");
}
}
- 消费者
package com.rj.bd.consumer;
import com.rabbitmq.client.Channel;
import com.rj.bd.Config.TopConfig;
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.io.IOException;
/**
* @author LXY
* @desc
* @time 2022--11--19--23:32
*/
@Slf4j
@Component
public class TopCunsumer {
@RabbitListener(queues= TopConfig.QUEUE_NAME1)
public void receiveD_queu(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
System.out.println("我应该匹配的是 #.to:"+msg);
}
@RabbitListener(queues= TopConfig.QUEUE_NAME2)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
System.out.println("我应该匹配的是 *.to.*"+msg);
}
}
- 生产者
@ApiOperation("Top发送消息")
@RequestMapping(value = "roleControllermessage/{mes}",method = RequestMethod.GET)
public void messsendop(@PathVariable("mes") String mes) {
System.out.println("发送绑定的key是 where.to.bing消息是:"+mes);
//指明交换机,只能where.to.bing的队列可以收到
rabbitTemplate.convertAndSend(TopConfig.TOP_EXCHANGE,"where.to.bing",mes);
}
十一:Rabbitmq的事务
- abbitMQ支持事务(transaction),RabbitMQ中与事务机制有关的方法有三个:txSelect(), txCommit()以及txRollback()。
(1)txSelect用于将当前channel设置成transaction模式,通过调用tx.select方法开启事务模式。
(2)txCommit用于提交事务。当开启了事务模式后,只有当一个消息被所有的镜像队列保存完毕后,RabbitMQ才会调用tx.commit-ok返回给客户端。
(3)txRollback用于回滚事务,在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。
事务确实能够解决producer与broker之间消息确认的问题,只有消息成功被broker接受,事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作同时进行消息重发。
事务机制的缺点 :
使用事务机制的话会降低RabbitMQ的性能。
会导致生产者和RabbitMq之间产生同步(等待确认),这也违背了我们使用RabbitMq的初衷,所以一般很少采用。
十二:消息的一致性
现在目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事务消息
这个问题也是问的最多的问题,消息的一致性和事务
推荐文章