1,什么是RabbitMQ?
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)
本质是个队列,FIFO 先入先出,只不过队列中存放的内容是 message 而已,还是一种跨进程的
通信机制,用于上下游传递消息
主要应用场景:
流量削峰 在收到大量请求时会将请求暂时放在rabbitmq中防止大量请求冲垮后台服务
应用解耦 分布式系统中使用rabbitmq来进行各模块之间的通信
异步处理 无需等待当前任务是否执行完毕便可以继续执行下面的任务
2,RabbitMQ的四大组成
生产者:用来产生消息
交换机:将生产者的消息分发到队列里面(没有声明交换机rabbitmq会使用默认的交换机)
队列:用来存放消息等着消费者来
消费者:消费队列里面的消息
3,消费者应答
为了保证消息在发送过程中不丢失消费者RabbitMQ引入了消费者应答 在消费接收到消息并将其处理完成后RabbitMQ便将其删除掉了
自动应答
消息一旦被发送出队列便认为消息已经成功消费
缺点:消息发送出后如果出现连接断开的情况消息就无法被成功消费了此时消息就丢失了
这种应答方式没限制应答数量可能会造成消费者的消息积压
手动应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(),boolean); 肯定确认 第一个参数表示 此条消息唯一标识符为long类型 第二个参数 如果为true则表示批量确认所有唯一标识符小于等于当前消息的唯一标识符都将会被认为已经消费成功 如果false则表示只确认当前消息 channel.basicNack(message.getMessageProperties().getDeliveryTag(),boolean,boolean); 拒绝消费 第二个参数为true表示批量拒绝消息为false只拒绝当前消息 第三个参数为true表示被拒绝的消息重新入队 false表示丢弃channel.basicReject(message.getMessageProperties().getDeliveryTag(),boolean); 拒绝消费与basicNack相比拒绝的消息直接被丢弃了 第二个参数表示是否批量拒绝
4,重新入队
消费者如果由于某种原因未发送ACK确认那么这条消息将会重新进入到队列中被其他消费者消费从而保证消息的不丢失
5,RabbitMQ持久化
RabbitMQ的持久化需要队列和消息同时持久化才能保证RabbitMQ的持久化
持久化是为了保证RabbitMQ上的消息在遇到服务器宕机时消费不会丢失 将消息保存在磁盘上
队列持久化:
未整合springboot的写法 channel.queueDeclare(name,true,false,false,null); 将第二个参数设为true
整合springboot的写法 QueueBuilder.durable(queueA).withArguments(hashMap).build();调用durable方法
消息持久化:
未整合springboot的写法
channel.basicPublish("",name,MessageProperties.PERSISTENT_TEXT_PLAIN,a.getBytes());
整合springboot的写法
rabbitTemplate.convertAndSend("exchange1", "normalA", "消息来自 C 为 "+ttl+"毫秒的队列: "+msg, message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
});
6,不公平分发和预取值
不公平分发:由于不同的消费者对消息的处理能力不同造成了分发相同数量的消息一部分消费者过早的处理完了处于空闲状态而另一部分还在处理积压的消息不公平分发很好的处理了这种现象做到了能者多劳
channel.basicQos(2);// 1消息采用不公平分发 能者多劳 数值大于1时表示为预取值
预取值:表示消费者一次可以从队列中获取的消息数量 这可以提高消费者的效率,因为它们可以在处理消息时减少网络延迟和通信开销。
7,发布确认
发布确认是指生产者在将消息发送到 RabbitMQ 服务器后,等待服务器确认消息已经被正确接收的过程 (消息被投递到匹配的队列上)
异步发布确认过程
1,channel.confirmSelect();//开启确认发布
2,
//添加发布确认监听器 消息发布到队列后会回调监听器里面的函数 第一个是成功的回调 第二个是失败的回调 channel.addConfirmListener((tag,mul)->{ //将发布成功的消息删除 此时集合里面只剩余未确认的 concurrentSkipListMap.remove(tag); System.out.println("消息发布成功"+tag); },(tag, mul)->{ String s = concurrentSkipListMap.get(tag); System.out.println("消息发布失败"+s+"编号"+tag); });
3,
//将发出的消息放在一个支持多线程和高并发的k-v集合中 第一个参数用来标识信息 concurrentSkipListMap.put(channel.getNextPublishSeqNo(),a);
这种方式虽然生产者可以收到确认消息但如果服务器突然出故障了那么这个消息就丢失了,所以我们需要把无法发布的消息保存起来重新发送 发布确认高级就解决了这个问题
8,发布确认高级
我们要在配置文件中开启发布确认 spring.rabbitmq.publisher-confirm-type=correlated
ConfirmCallback 用于处理消息的确认机制,当生产者将消息发送到 服务器,服务器 确认接收到消息时,就会调用 ConfirmCallback 的confirm回调方法,如果消息发送失败,则会调用 ConfirmCallback 回调方法的失败处理逻辑。使用 channel.addConfirmListener() 方法或 RabbitTemplate.setConfirmCallback() 方法可以注册 ConfirmCallback 回调(只要消息发送便会会回调ConfirmCallback的confirm方法)
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
//CorrelationData对象需要我们在发送消息时自己创建
// CorrelationData correlationData1 = new CorrelationData();
//correlationData1.setId("1");
//rabbitTemplate.convertAndSend("confirmExchange","key12",message,correlationData1);
if(ack){
log.info("id为{}的消息发送到交换机成功",correlationData.getId());
}else {
log.info("id为{}的消息发送到交换机失败",correlationData.getId());
}
});
上面的操作只是确保了消息到交换机的不丢失 如果交换机发消息给队列发现路由不可用也会将消息丢失,这时我们需要进行回退操作
在配置文件打开回退 spring.rabbitmq.publisher-returns=true
消息无法由交换机发送到路由器时便会调用ReturnCallback的 returnedMessage方法
rabbitTemplate.setReturnCallback((message1,replyCode, replyText, exchange,routingKey)->{
log.error("消息{}的交换机为{}routing-key为{}因为{}原因被回退",new String(message1.getBody()),exchange,routingKey,replyText);
});
9,交换机
用于将生产者所产生的消息分发到队列上
1,direct交换机
发送消息时根据routing_key将消息分发到绑定了指定路由key的队列上
2,fanout交换机
发送的消息所有的队列都能接受到
3,topic交换机
routing_key并不是随意写的
它必须是一个单 词列表,以点号分隔开
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
//消费者代码
Channel channel = MyUtils.getChannel();
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机并且声明routing_key
channel.queueBind(queue,"topic1","k1.#");
channel.basicConsume(queue,true,(s,delivery)->{
System.out.println("C2输出"+new String(delivery.getBody()));
},(s)->{});
//生产者代码
Channel channel = MyUtils.getChannel();
channel.exchangeDeclare("topic1", BuiltinExchangeType.TOPIC);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String next = scanner.next();
channel.basicPublish("topic1","k1",null,next.getBytes());
}
10,死信队列
死信队列的主要作用:来保障消息至少被消费一次以及未被正 确处理的消息不会被丢弃
无法被消费的信息将会放到死信队列中 1,到达了消息的过期时间 2,队列达到最大长度 3,被拒绝应答并且无法重新入队的消息
声明:在normal-queue中完成声明死信交换机和routing-key 这样死信消息才可以进入到死信队列中
由于队列先来先服务的特性在队列中如果1号信息的过期时间为10秒而2号信息的过期时间为3秒那么在2号过期时间达到了之后并不会立即进入到死信队列因为1号消息的过期时间没到 1号没执行就不会执行2号 因此死信队列不适合做消息延迟使用
11,延迟队列
延迟队列的实现需要基于rabbitmq_delayed_message_exchange 插件实现
我们需要自定义交换机代码如下
@Bean
public CustomExchange customExchange(){
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("x-delayed-type","direct");//插件中的一个参数,用于定义交换机类型
// direct:直接匹配模式。
// topic:主题匹配模式。
// fanout:广播模式。
return new CustomExchange(delay_exchange,"x-delayed-message",true,false,hashMap);
}
12,备份交换机
在发布确认高级中生产者已经可以获取发送不成功的消息,如果是些无法路由的消息生产者重发消息也会再次失败,死信队列也无法解决这种情况因为消息都无法进入到队列中去,此时备份交换机就很好的解决了这个问题
注:备份交换机的类型为fanout
主要代码如下
@Bean
public DirectExchange exchange(){
return ExchangeBuilder.directExchange("confirmExchange").durable(true)
//添加备份交换机
.withArgument("alternate-exchange","backupExchange").build();
}
@Bean
public Queue queue(){
return QueueBuilder.durable("confirmQueue").build();
}
@Bean
public Binding binding(DirectExchange exchange,Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
//声明备份交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("backupExchange");
}
//声明备份队列
@Bean
public Queue backupQueue(){
return QueueBuilder.durable("backupQueue").build();
}
//绑定备份交换机和备份队列
@Bean
public Binding fanoutExchangeBindingBackupQueue(@Qualifier("backupQueue")Queue queue,FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
备份交换机的优先级要高于发布回退的ReturnCallback的回调方法
13,幂等性问题
消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但 实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
redis 可以很好地解决 RabbitMQ 的幂等性问题,具体的实现方式如下:
1. 在生产者发送消息时,生成一个唯一标识符(例如消息 ID),并将其与消息一起存储在 Redis 中。
2. 在消费者接收到消息后,先在 Redis 中查找该消息的唯一标识符是否已经存在。
3. 如果存在,则说明该消息已经被处理过,直接返回处理结果即可;如果不存在,则将该消息的唯一标识符存储到 Redis 中,并进行消息处理。
4. 在 Redis 中设置该消息的唯一标识符的过期时间,避免过多占用内存。
14,优先级队列
RabbitMQ是先来先服务的一个队列,当队列中有些消息需要被优先消费时我们就需要优先级队列
优先级队列的实现:设置消息的优先级 所有消息全部进入到队列后会进行优先级排列 优先级越大越靠前就优先被消费 队列也要设置优先级表示这个队列所能接受的消息最大优先级一般设置为10
代码实现
public Queue queue(){
//设置队列优先级
HashMap<String, Object> hashMap = new HashMap();
hashMap.put("x-max-priority",10);
return QueueBuilder.durable("confirmQueue").withArguments(hashMap).build();
}
//设置消息优先级
Message message=MessageBuilder.withBody((message0+i).getBytes(StandardCharsets.UTF_8))
.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN).setPriority(5).build();
rabbitTemplate.convertAndSend("confirmExchange","key1",message,correlationData1);
15,惰性队列
惰性队列会将消息存放在磁盘中只有消费者来消费消息的时候才会将消息放到内存中,虽然惰性队列会降低消息消费的速度但是在消费者突然宕机大量消息无法消费时惰性队列的就非常有用了
惰性队列声明代码
HashMap<String, Object> hashMap = new HashMap();
hashMap.put(("x-queue-mode", "lazy");
return QueueBuilder.durable("confirmQueue").withArguments(hashMap).build();