手动应答
channel.basicAck(deliverTag,true);//true 为批量应答
需要开始multiple,手动应答模式
对列的吃持久化
/*
* 如果队列不存在,则会创建
* Rabbitmq不允许创建两个相同的队列名称,否则会报错。
*
* @params1: queue 队列的名称
* @params2: durable 队列是否持久化
* @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
* @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
* */
// 这里如果queue已经被创建过一次了,可以不需要定义
channel.queueDeclare("queue1", false, false, false, null);
channel收到消息的回调
Channel finalChannel = channel;
finalChannel.basicConsume(queueName, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println(queueName + ":收到消息是:" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
消息持久化
MessageProperties.PERSISTENT_TEXT_PLAIN
//MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublicsh("交换机","队列名称",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
不公平分发
消费者设置
channel.basicQos(1);//0:公平分发 1:不公平分发--建议 >1 就是预期值
延时队列 另一种用法
但是如果有多条消息,会排队,这时只会查看第一条消息的过期时间,不会管第二条
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间:{},发送一条时长{}毫秒TTL信息给队列QC:{}",
new Date().toString(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message,msg->{
//发送消息的时候 延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
- 下载延迟插件
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.8.9/rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez
- 将延迟插件放到RabbitMQ的插件目录下:
需要下载延时插件来解决此问题,此插件安装成功后,交换机的type会多一个延时类型
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
systemctl restart rabbitmq-server
@Configuration
public class DelayedQueueConfig {
//队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//routingKey
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
//声明队列
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
};
//声明交换机
@Bean
public CustomExchange delayedExchange(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-delayed-type","direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",
true,false,arguments);
}
//绑定
@Bean
public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue,
@Qualifier("delayedExchange") CustomExchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
上面代码修改
//开始发消息 基于插件的 消息 及 延迟的时间
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间:{},发送一条时长{}毫秒信息给延迟队列delayed.queue:{}",
new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME
,DelayedQueueConfig.DELAYED_ROUTING_KEY,message,msg -> {
// 发送消息的时候 延迟时长 单位ms
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
交换机回调机制
配置增加
spring.rabbitmq.publisher-confirm-type = correlated
配置了
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
}
/*
* 交换机确认回调方法,发消息后,交换机接收到了就回调
* 1.1 correlationData:保存回调消息的ID及相关信息
* 1.2 b:交换机收到消息,为true
* 1.3 s:失败原因,成功为null
*
* 发消息,交换机接受失败,也回调
* 2.1 correlationData:保存回调消息的ID及相关信息
* 2.2 b:交换机没收到消息,为false
* 2.3 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);
}
}
}
消息回退
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
yml
spring.rabbitmq.publisher-returns=true
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/*
* 交换机确认回调方法,发消息后,交换机接收到了就回调
* 1.1 correlationData:保存回调消息的ID及相关信息
* 1.2 b:交换机收到消息,为true
* 1.3 s:失败原因,成功为null
*
* 发消息,交换机接受失败,也回调
* 2.1 correlationData:保存回调消息的ID及相关信息
* 2.2 b:交换机没收到消息,为false
* 2.3 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(ReturnedMessage returnedMessage) {
log.error("消息{},被交换机{}退回,退回的原因:{},路由Key:{}",
new String(returnedMessage.getMessage().getBody())
, returnedMessage.getExchange()
, returnedMessage.getReplyText()
, returnedMessage.getRoutingKey());
}
}
备份交换机
有了mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ.中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
// 配置类:发布确认(高级)
@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 DirectExchange confirmExchange(){//durable 持久化 alternate-exchange备份交换机名字
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定
@Bean
public Binding queueBindingExchange(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange")DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_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 warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
}
消费者
// 报警消费者
@Component
@Slf4j
public class WarningConsumer {
//接受报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}",msg);
}
}
mandatory参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高
幂等性
-
MQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费者消费MQ中的消息也可利用MQ的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。
-
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一ID+指纹码机制,利用数据库主键去重, b.利用redis.的原子性去实现
-
唯一ID+指纹码机制:
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保 证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是 单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
-
Redis原子性:
利用redis执行setnx命令,天然具有幂等性,从而实现不重复消费
-
优先级队列
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序
public class Producer {
// 队列名称
public static final String QUEUE_NAME="hello";
// 发消息
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP连接RabbitMQ的队列
factory.setHost("192.168.163.128");
// 用户名
factory.setUsername("admin");
// 密码
factory.setPassword("123");
// 创建连接
Connection connection = factory.newConnection();
// 获取信道
Channel channel = connection.createChannel();
Map<String, Object> arguments = new HashMap<>();
//官方允许是0-255之间,此处设置10,允许优化级范围为0-10,不要设置过大,浪费CPU与内存
arguments.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);
// 发消息
for (int i = 0; i < 10; i++) {
String message = "info" + i;
if(i == 5){
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes(StandardCharsets.UTF_8));
}else {
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
}
}
System.out.println("消息发送完毕!");
}
}
生产者
public class Consumer {
// 队列名称
public static final String QUEUE_NAME = "hello";
// 接受消息
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.163.128");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明 接受消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
System.out.println(new String(message.getBody()));
};
// 声明 取消消息
CancelCallback cancelCallback = consumer -> {
System.out.println("消息消费被中断");
};
System.out.println("C2等待接受消息.......");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ISdLgHyL-1639404532263)(C:\Users\qubing\AppData\Roaming\Typora\typora-user-images\image-20211213215155704.png)]
惰性队列
使用场景:
RabbitMQ从 3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
两种模式:
队列具备两种模式: default和lazy.默认的为default模式,在3.6.0之前的版本无需做任何变更。lazy_模式即为惰性队列的模式,可以通过调用channel.queueDecare方法的时候在参数中设置,也可以通过
Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过"x-queue-mode"参数来设置队列的模式,取值为"default"和"lazy”"。下面示例中演示了一个惰性队列的声明细节:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode","lazy");
channel.queueDeclare( "myqueue", false, false, false,args);
集群
以确保各个节点的cookie文件使用的是同一个值:在node1上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
启动RabbitMQ服务,顺带启动Erlang虚拟机和RabbitMQ应用服务,三台节点下执行:
rabbitmq-server -detached
在节点2执行
# rabbitmqctl stop会将Erlang虚拟机关闭 rabbitmqctl stop_app 只关闭rabbitmq服务
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
# 只启动rabbitmq服务
rabbitmqctl start_app
在节点3执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
rabbitmqctl cluster_status
查看集群状态
之后在三个集群节点的任意一个可视化界面登录均可
接触集群节点,node2和node3分别执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
# 此项命令均在node1上执行
rabbitmqctl forget_cluster_node rabbit@node2
镜像队列
使用原因
如果RabbitMQ集群中只有一个Broker节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过publisherconfirm机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。
搭建步骤
- 启动三台集群节点
- 随便找一个节点添加policy
- 在node1创建一个队列发噢是那个一条信息,队列存在镜像队列
- 在node1上创建一个队列发送一条消息,队列存在镜像队列
实现高可用负载均衡
HAProxy.提供高可用性、负载均衡及基于TCPHTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub.在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。
扩展nginx,lvs,haproxx.p间的区别: http://www.ha97.com/5646.html
Federation Exchange
使用原因
(broker北京),(broker深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client北京)需要连接(broker北京),向其中的交换器exchangeA.发送消息,此时的网络延迟很小,(Client北京)可以迅速将消息发送至exchangeA.中,就算在开启了publisherconfirm.机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client深圳)需要向exchangeA发送消息,那么(Client深圳)(broker北京)之间有很大的网络延迟,(Client深圳)将发送消息至exchangeA会经历一定的延迟,尤其是在开启了publisherconfirm.机制或者事务机制的情况下,(Client深圳)会等待很长的延迟时间来接收(broker北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。
将业务(Client深圳)部署到北京的机房可以解决这个问题,但是如果(Client深圳)调用的另些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用Federation插件就可以很好地解决这个问题.
搭建步骤
-
需要保证每台节点单独运行
-
在每台机器上开启federation相关插件
每台节点均需执行以下命令
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugns enable rabbitmq_federation_management
- 原理图
-
在下游节点(node2)配置上游节点(node1)
-
添加policy
-
成功
Federation Queue
使用原因
联邦队列可以在多个Broker节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息的需求
- 添加上下游配置(同10.4)
- 添加policy
Shovel
使用原因
Federation具备的数据转发功能类似,Shovel够可靠、持续地从一个Broker中的队列(作为源端,即source)拉取数据并转发至另一个Broker中的交换器(作为目的端,即destination)。作为源端的队列和作为目的端的交换器可以同时位于同一个Broker,也可以位于不同的Broker上。Shovel可以翻译为"铲子",是一种比较形象的比喻,这个"铲子"可以将消息从一方"铲子"另一方。Shovel行为就像优秀的客户端应用程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。
搭建步骤
-
开启插件(需要的机器都开启)
rabbitmq-plugins enable rabbitmq_shovel rabbitmq-plugins enable rabbitmq_shovel_management 12
-
添加shevel源和目的地