11.RabbitMQ
一、RabbitMQ介绍
1.1 现存问题
- 服务调用:两个服务调用时,我们可以通过传统的HTTP方式,让服务A直接去调用服务B的接口,但是这种方式是同步的方式,虽然可以采用SpringBoot提供的@Async注解实现异步调用,但是这种方式无法确保请求一定会访问到服务B的接口。那如何保证服务A的请求信息一定能送达到服务B去完成一些业务操作呢?| 如何实现异步调用
- 海量请求:在我们在做一些秒杀业务时,可能会在某个时间点突然出现大量的并发请求,这可能已经远远超过服务器的并发瓶颈,这时我们需要做一些削峰的操作,也就是将大量的请求缓冲到一个队列中,然后慢慢的消费掉。如何提供一个可以存储千万级别请求的队列呢?
- 在微服务架构下,可能一个业务会出现同时调用多个其他服务的场景,而且这些服务之间一般会用到Feign的方式进行轻量级的通讯,如果存在一个业务,用户创建订单成功后,还需要去给用户添加积分、通知商家、通知物流系统、扣减商品库存,而在执行这个操作时,如果任意一个服务出现了问题,都会导致整体的下单业务失败,并且会导致给用户反馈的时间延长。这时就造成了服务之间存在一个较高的耦合性的问题。如何可以降低服务之间的耦合性呢?
1.2 处理问题
RabbitMQ就可以解决上述的全部问题
- 服务之间如何想实现可靠的异步调用,可以通过RabbitMQ的方式实现,服务A只需要保证可以把消息发送到RabbitMQ的队列中,服务B就一定会消费到队列中的消息只不过会存在一定的延时。| 异步访问
- 忽然的海量请求可以存储在RabbitMQ的队列中,然后由消费者慢慢消费掉,RabbitMQ的队列本身就可以存储上千万条消息
- 在调用其他服务时,如果允许延迟效果的出现,可以将消息发送到RabbitMQ中,再由消费者慢慢消费| 服务解耦
1.3 RabbitMQ介绍
百度百科:
RabbitMQ是实现了高级消息队列协议(AMQP:消息中间件的专用协议)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
首先RabbitMQ基于AMQP协议开发,所以很多基于AMQP协议的功能RabbitMQ都是支持的,比如SpringCloud中的消息总线bus
其次RabbitMQ是基于Erlang编写,这是也是RabbitMQ天生的优势,Erlang被称为面向并发编程的语言,并发能力极强,在众多的MQ中,RabbitMQ的延迟特别低,在微秒级别,所以一般的业务处理RabbitMQ比Kafka和RocketMQ更有优势。
最后RabbitMQ提供自带了图形化界面,操作方便,还自带了多种集群模式,可以保证RabbitMQ的高可用,并且SpringBoot默认就整合RabbitMQ,使用简单方便。
二、 安装RabbitMQ
docker安装
在linux的docker里拉取RabbitMQ镜像 并运行容器(management是带web的管理界面)。
docker pull rabbitmq:3.8.3-management
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 镜像id
5672是客户端和RabbitMQ进行通信的端口。
15672是管理界面访问web页面的端口。
进入容器环境
docker exec -it 容器id /bin/bash
在进入容器环境后开启rabbitmq_management 执行:
rabbitmq-plugins enable rabbitmq_management
在浏览器中输入http://192.168.1.201:15672/访问RabbitMQ的管理页面,用户名和密码默认guest。(192.168.1.201我linux的IP地址)
windows安装
安装erlang和rabbitmq后,进入rabbitmq安装目录的sbin目录执行下面命令
rabbitmq-plugins.bat enable rabbitmq_management
杀进程
tasklist | find /i "erl"taskkill /pid 7300 -t -f
启动服务
双击 rabbitmq-server.bat
访问 http://localhost:15672 用户名密码:guest
三、RabbitMQ构架
RabbitMQ的架构可以查看官方地址:https://rabbitmq.com/tutorials/amqp-concepts.html
官方简单架构
可以看出RabbitMQ中主要分为三个角色:
- Publisher:消息的发布者,将消息发布到RabbitMQ中的Exchange
- RabbitMQ服务:Exchange接收Publisher的消息,并且根据Routes策略将消息转发到Queue中
- Consumer:消息的消费者,监听Queue中的消息并进行消费
官方提供的架构图相对简洁,我们可以自己画一份相对完整一些的架构图:
RabbitMQ架构图
可以看出Publisher和Consumer都是单独和RabbitMQ服务中某一个Virtual Host建立Connection的客户端
后续通过Connection可以构建Channel通道,用来发布、接收消息
一个Virtual Host中可以有多个Exchange和Queue,Exchange可以同时绑定多个Queue
在基于架构图查看图形化界面,会更加清晰
图形化界面信息
四、RabbitMQ通讯方式
RabbitMQ提供了很多中通讯方式,依然可以去官方查看:https://rabbitmq.com/getstarted.html
七种通讯方式
- Hello World!:为了入门操作!
- Work queues:一个队列被多个消费者消费
- Publish/Subscribe:手动创建Exchange(FANOUT)
- Routing:手动创建Exchange(DIRECT)
- Topics:手动创建Exchange(TOPIC)
- RPC:RPC方式
- Publisher Confirms:保证消息可靠性
五、SpringBoot整合
5.1 Hello World
通讯方式
RabbitConfig
@Configuration
public class RabbitConfig {
// 一个队列 一个生产者 一个消费者
@Bean
public Queue helloQueue(){
// 参数1:mq中队列名称
// 参数2:是否持久化队列,关闭服务后队列是否还存在 默认值false
// 参数3:是否为独占连接队列 默认值 false
// 参数4:服务器停止是否自动删除 默认值 false
// 参数5:队列参数
return new Queue("helloQueue");
}
}
生产者:
@Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send (String msg){
rabbitTemplate.convertAndSend("helloQueue",msg);
}
}
消费者:
@Component
public class Consumer {
// @RabbitListener 自动监听指定队列,发现有消息会自动获取信息赋给方法形参
@RabbitListener(queues = {"helloQueue"})
public void consume(String msg){
System.out.println("消费者收到消息:" + msg);
}
}
控制器测试
@RestController
public class RabbitController {
@Resource
private Publisher publisher;
@GetMapping("/hello")
public String hello(String msg){
publisher.send(msg);
return "ok";
}
}
5.2 Work Queues
WorkQueues需要学习的内容
RabbitConfig
// 一个队列 一个生产者 多个消费者
@Bean
public Queue workQueue(){
return new Queue("workQueue");
}
生产者
Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg){
for (int i = 0; i < 100; i++) {
rabbitTemplate.convertAndSend("workQueue",msg+i);
}
}
}
消费者
@Component
public class Consumer {
@RabbitListener(queues = {"workQueue"})
public void consume1(String msg) throws IOException {
System.out.println("消费者1收到消息:" + msg);
}
@RabbitListener(queues = {"workQueue"})
public void consume2(String msg) throws IOException {
System.out.println("消费者2收到消息:" + msg);
}
}
5.3 Publish/Subscribe
自定义一个交换机
Exchange交换机:只负责转发消息,不具备存储消息的能力,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
config
//一个生产者 - 一个交换机(Fanout) - 多个队列 - 多个消费者
// 交换机收到消息,会把消息转发给绑定的所有队列
@Bean
public Queue fanoutQueueA(){
return new Queue("fanoutQueueA");
}
@Bean
public Queue fanoutQueueB(){
return new Queue("fanoutQueueB");
}
@Bean
public FanoutExchange fanoutExchange(){
// 参数1:交换机名称
// 参数2:是否持久化 默认值true
// 参数3:是否自动删除 没有队列绑定时是否自动删除 默认值false
return new FanoutExchange("fanoutExchange");
}
// 绑定交换机和队列
@Bean
public Binding fanoutQueueABinding(){
return BindingBuilder.bind(fanoutQueueA()).to(fanoutExchange());
}
@Bean
public Binding fanoutQueueBBinding(@Autowired Queue fanoutQueueB,
@Autowired FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueueB).to(fanoutExchange);
}
生产者
@Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("fanoutExchange",null,msg+i);
}
}
}
消费者
@Component
public class Consumer {
@RabbitListener(queues = {"fanoutQueueA"})
public void consume1(String msg){
System.out.println("消费者1收到 A 队列的消息:" + msg);
}
@RabbitListener(queues = {"fanoutQueueA"})
public void consume2(String msg){
System.out.println("消费者2收到 A 队列的消息:" + msg);
}
@RabbitListener(queues = {"fanoutQueueB"})
public void consume3(String msg){
System.out.println("消费者3收到 B 队列的消息:" + msg);
}
@RabbitListener(queues = {"fanoutQueueB"})
public void consume4(String msg){
System.out.println("消费者4收到 B 队列的消息:" + msg);
}
}
5.4 Routing
DIRECT类型Exchange
生产者:在绑定Exchange和Queue时,需要指定好routingKey,同时在发送消息时,也指定routingKey,只有routingKey一致时,才会把指定的消息路由到指定的Queue
@Bean
public Queue directQueueA(){
return new Queue("directQueueA");
}
@Bean
public Queue directQueueB(){
return new Queue("directQueueB");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding directQueueABinding(){
return BindingBuilder.bind(directQueueA()).to(directExchange()).with("A");
}
@Bean
public Binding directQueueBBinding(){
return BindingBuilder.bind(directQueueB()).to(directExchange()).with("B");
}
@Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg){
for (int i = 0; i < 10; i++) {
if(i % 3 == 0){
rabbitTemplate.convertAndSend("directExchange","A",msg+i);
}else if(i % 3 == 1){
rabbitTemplate.convertAndSend("directExchange","B",msg+i);
}else{
rabbitTemplate.convertAndSend("directExchange","C",msg+i);
}
}
}
}
5.5 Topic
Topic模式
生产者:TOPIC类型可以编写带有特殊意义的routingKey的绑定方式
config
@Bean
public Queue topicQueueA(){
return new Queue("topicQueueA");
}
@Bean
public Queue topicQueueB(){
return new Queue("topicQueueB");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Binding topicQueueABinding(){
// * 段数必须一致,段内字符任意
return BindingBuilder.bind(topicQueueA()).to(topicExchange()).with("A.*.*");
}
@Bean
public Binding topicQueueBBinding(){
// # 通配
return BindingBuilder.bind(topicQueueB()).to(topicExchange()).with("B.#");
}
生产者
@Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg,String routeingKey){
rabbitTemplate.convertAndSend("topicExchange",routeingKey,msg);
}
}
controller
@GetMapping("/topic/{key}")
public String topic(String msg, @PathVariable("key")String key){
publisher.send(msg,key);
return "ok";
}
5.6 RPC(了解)
因为两个服务在交互时,可以尽量做到Client和Server的解耦,通过RabbitMQ进行解耦操作
需要让Client发送消息时,携带两个属性:
- replyTo告知Server将相应信息放到哪个队列
- correlationId告知Server发送相应消息时,需要携带位置标示来告知Client响应的信息
RPC方式
客户端:
package com.wang.rpc;
import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
import java.util.UUID;
public class Publisher {
public static final String QUEUE_PUBLISHER = "rpc_publisher";
public static final String QUEUE_CONSUMER = "rpc_consumer";
@Test
public void publish() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);
//4. 发布消息
String message = "Hello RPC!";
String uuid = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.replyTo(QUEUE_CONSUMER)
.correlationId(uuid)
.build();
channel.basicPublish("",QUEUE_PUBLISHER,props,message.getBytes());
channel.basicConsume(QUEUE_CONSUMER,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String id = properties.getCorrelationId();
if(id != null && id.equalsIgnoreCase(uuid)){
System.out.println("接收到服务端的响应:" + new String(body,"UTF-8"));
}
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
System.out.println("消息发送成功!");
System.in.read();
}
}
服务端:
package com.wang.rpc;
import com.wang.helloworld.Publisher;
import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
public class Consumer {
public static final String QUEUE_PUBLISHER = "rpc_publisher";
public static final String QUEUE_CONSUMER = "rpc_consumer";
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建队列
channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);
//4. 监听消息
DefaultConsumer callback = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取到消息:" + new String(body,"UTF-8"));
String resp = "获取到了client发出的请求,这里是响应的信息";
String respQueueName = properties.getReplyTo();
String uuid = properties.getCorrelationId();
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.correlationId(uuid)
.build();
channel.basicPublish("",respQueueName,props,resp.getBytes());
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume(QUEUE_PUBLISHER,false,callback);
System.out.println("开始监听队列");
System.in.read();
}
}
六、RabbitMQ保证消息可靠性
生产者端
6.1 保证消息一定送达到Exchange
Confirm机制:可以通过Confirm效果保证消息一定送达到Exchange,官方提供了三种方式,选择了对于效率影响最低的异步回调的效果
生产者把消息发给交换机,交换机通过一个回调函数告诉生产者消息是否送达,如果没有送达,生产者可以做出补偿,如消息重发,或者保存消息,无论成功与否都会触发回调函数
打开交换机的消息确认机制,默认是关闭的
spring:
rabbitmq:
host: localhost #主机IP
port: 5672 #端口号 15672是可视化界面的端口号
username: guest #用户名
password: guest #密码
virtual-host: / #虚拟主机
publisher-confirm-type: correlated #发布消息成功到交换器后会触发回调方法
生产者实现RabbitTemplate.ConfirmCallback接口,重写confirm方法
@Component
public class Publisher implements RabbitTemplate.ConfirmCallback{
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg,String routeingKey){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.convertAndSend("topicExchange11",routeingKey,msg);
}
//correlationData 生产者发送消息时封装的类,消息发送失败时,用这个类进行消息重发 或保存消息
// flag 失败或成功的标识
// reason 失败的原因
@Override
public void confirm(CorrelationData correlationData, boolean flag, String reason) {
if(!flag){
// 消息投递失败,做补偿措施,可以重发 或者 将消息保存到数据库
}
System.out.println(correlationData);
System.out.println(flag);
System.out.println(reason);
}
}
更改交换机的name,制造一个投递交换机失败的错误,reason会打印出失败的结果
6.2 保证消息可以路由到Queue
Return机制
为了保证Exchange上的消息一定可以送达到Queue,通过队列的回调函数,只有发送失败才会触发回调函数
spring:
rabbitmq:
host: localhost #主机IP
port: 5672 #端口号 15672是可视化界面的端口号
username: guest #用户名
password: guest #密码
virtual-host: / #虚拟主机
publisher-confirm-type: correlated #交换机消息确认机制
publisher-returns: true #队列的消息确认机制
生产者实现RabbitTemplate.ReturnsCallback接口,重写returnedMessage方法
@Component
public class Publisher implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg,String routeingKey){
rabbitTemplate.setReturnsCallback(this); // 使用ReturnsCallback
rabbitTemplate.convertAndSend("topicExchange",routeingKey,msg);
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println(returnedMessage.getMessage()); // 发送的消息内容
System.out.println(returnedMessage.getReplyCode()); // 失败状态码
System.out.println(returnedMessage.getReplyText()); // 失败原因
System.out.println(returnedMessage.getExchange()); // 交换机名称
System.out.println(returnedMessage.getRoutingKey()); // 路由
}
}
消费者端
6.3 保证消费者可以正常消费消息
消费者从队列中获取消息,是将消息复制一份进行消费,消费完成后会给队列一个自动ack(应答),队列只要收到ack,删除队列中已消费的消息,队列只管应答,不管消费是否正常完成,如果消费者端出现异常情况,消息没有被正常消费,队列依旧收到自动应答,删除消息,那么消息丢失,所以想解决这个问题,我们需要将自动应答改为手动ack,确保消费者正常消费之后才给队列应答,保证消息不丢失
spring:
rabbitmq:
host: localhost #主机IP
port: 5672 #端口号 15672是可视化界面的端口号
username: guest #用户名
password: guest #密码
virtual-host: / #虚拟主机
publisher-confirm-type: correlated #交换机消息确认机制
publisher-returns: true #队列的消息确认机制
listener:
simple:
acknowledge-mode: manual #开启消费者的手动ack
prefetch: 1 #消费者一次获取的消息数量 默认是250
开启消费者手动ack之后,队列中的消息虽然被消费,但是还会存在消息,状态为Unacked未应答状态,表示消息还在队列中
// 消息传输通道 RabbitMQ和生产者/消费者之间的Connection相当于是一个进程,
// channel是进程中的线程,也是消息传输通道
// Message 消息封装类 包含了消息体,以及消息的一些属性,如字符集 大小 过期时间 优先级等
@RabbitListener(queues = {"topicQueueB"})
public void consume2(String msg, Channel channel, Message message) {
try {
// int i = 1 / 0 ;
System.out.println("消费者2收到 B 消息:" + msg);
// 消费者手动应答
// 参数1:消息的标识 参数2:是否批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
try {
// 消费者的拒绝应答 队列只接受应答不管是否是成功的应答,都会删除消息,所以需要确认是否放回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), // 消息标识
false, // 是否批量处理
true); // 是否将消息重新放回队列
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
6.4 保证Queue可以持久化消息
DeliveryMode设置消息持久化
DeliveryMode设置为2代表持久化,如果设置为1,就代表不会持久化。
//7. 设置消息持久化
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.deliveryMode(2)
.build();
//7. 发布消息
channel.basicPublish("","confirms",true,props,message.getBytes());
七、RabbitMQ死信队列&延迟交换机
7.1 什么是死信
死信&死信队列
死信队列的应用:
- 基于死信队列在队列消息已满的情况下,消息也不会丢失
- 实现延迟消费的效果。比如:下订单时,有15分钟的付款时间
7.2 实现死信队列
7.2.1 准备Exchange&Queue
package com.wang.config;
import com.rabbitmq.client.AMQP;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Classname DeadLetterConfig
* @Description TODO
* @Date 2025/4/1 17:08
* @Created by pc
*/
@Configuration
public class DeadLetterConfig {
public static final String NORMAL_EXCHANGE = "normal-exchange";
public static final String NORMAL_QUEUE = "normal-queue";
public static final String NORMAL_ROUTING_KEY = "normal.#";
public static final String DEAD_EXCHANGE = "dead-exchange";
public static final String DEAD_QUEUE = "dead-queue";
public static final String DEAD_ROUTING_KEY = "dead.#";
@Bean
public Exchange normalExchange(){
return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE).build();
}
@Bean
public Queue normalQueue(){
// 如果拒绝要指定死信交换机并重新指定死信路由
return QueueBuilder.durable(NORMAL_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE). // 指定死信队列
deadLetterRoutingKey(DEAD_ROUTING_KEY) // 指定死信路由
.build();
}
@Bean
public Binding normalBinding(Queue normalQueue,Exchange normalExchange){
return BindingBuilder.bind(normalQueue).to(normalExchange).with(NORMAL_ROUTING_KEY).noargs();
}
@Bean
public Exchange deadExchange(){
return ExchangeBuilder.topicExchange(DEAD_EXCHANGE).build();
}
@Bean
public Queue deadQueue(){
return QueueBuilder.durable(DEAD_QUEUE).build();
}
@Bean
public Binding deadBinding(Queue deadQueue,Exchange deadExchange){
return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
}
}
7.2.2 实现效果
- 基于消费者进行reject或者nack实现死信效果
package com.wang.dead;
import com.wang.config.DeadLetterConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
/**
* @Classname DeadPublisher
* @Description 消息生产者
* @Date 2025/4/1 18:40
* @Created by pc
*/
@SpringBootTest
public class DeadPublisher {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void publish(){
String message = "dead Letter";
rabbitTemplate.convertAndSend(DeadLetterConfig.NORMAL_EXCHANGE,"normal.123",message);
}
}
package com.wang.dead;
import com.rabbitmq.client.Channel;
import com.wang.config.DeadLetterConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class DeadListener {
@RabbitListener(queues = DeadLetterConfig.NORMAL_QUEUE)
public void consume(String mes, Channel channel, Message message) throws IOException {
System.out.println("接受到normal队列的消息:" + message);
// 方式一: 使用拒绝 并在requeue为false的情况
// 获取消息标识 设置requeue 表示不需要重新放回队列中
// channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
// 方式二: 使用Nack方式
// 第二个参数是否为批量操作
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}
}
-
生存时间
- 给消息设置生存时间
@Test public void publishExpire(){ String message = "dead Letter expire"; rabbitTemplate.convertAndSend(DeadLetterConfig.NORMAL_EXCHANGE, "normal.123", message, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties() .setExpiration("5000"); // 设置消息的过期时间为5000毫秒 return message; } }); }
- 给队列设置消息的生存时间
@Bean public Queue normalQueue(){ return QueueBuilder.durable(NORMAL_QUEUE) .deadLetterExchange(DEAD_EXCHANGE) .deadLetterRoutingKey("dead.123") .ttl(10000) // 给队列中的消息设置生存时间 .build(); }
-
设置Queue中的消息最大长度
@Bean public Queue normalQueue(){ return QueueBuilder.durable(NORMAL_QUEUE) .deadLetterExchange(DEAD_EXCHANGE) .deadLetterRoutingKey("dead.123") .maxLength(1) // 给队列 .build(); }
只要Queue中已经有一个消息,如果再次发送一个消息,这个消息会变为死信!
7.3 延迟交换机
死信队列实现延迟消费时,如果延迟时间比较复杂,比较多,直接使用死信队列时,需要创建大量的队列还对应不同的时间,可以采用延迟交换机来解决这个问题。
使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺如果十天内没有上传过商品,则会自动发消息提醒;
- 账号注册成功,如果三天内没登陆则进行短信提醒
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
安装之前可以查看交换机可选类型
下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9
安装延迟交换机
docker
将下载插件传到虚拟机中,并拷贝到rabbitMQ容器中
docker cp /home/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/opt/rabbitmq/plugins
进入到容器中查看是否复制成功
docker exec -it rabbitmq /bin/bash
cd /opt/rabbitmq/plugins
跳到sbin目录下去执行这个插件
cd ../sbin/
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
docker restart rabbitmq
windows
- 把插件放入rabbitmq安装目录的plugins目录
- 进入rabbitmq 安装目录的sbin 目录
- 执行下面命令让改插件生效
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启RabbitMQ服务后,可以看到交换机中多了一种类型的交换机
代码实现
-
构建延迟交换机
// 延迟队列 @Bean public Queue delayedQueue(){ return new Queue("delayedQueue"); } // CustomExchange 自定义交换机 @Bean public CustomExchange delayedExchange(){ Map<String,Object> map = new HashMap<>(); // 指定该交换机的类型 map.put("x-delayed-type","topic"); return new CustomExchange("delayedExchange", //交换机名称 "x-delayed-message", // 交换机类型 true, // 是否持久化 没有被消费的消息是否持久化 false, // 没有队列绑定到交换机是否删除 map); // 其他参数 } @Bean public Binding delayedQueueBinding(){ return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with("delay").noargs(); }
-
发送消息
@Component
public class Publisher {
@Resource
private RabbitTemplate rabbitTemplate;
public void send(String msg,int timeout){
rabbitTemplate.convertAndSend("delayedExchange", // 交换机名称
"delay", // routingKey
msg, // 消息内容
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的过期时间 单位是毫秒
message.getMessageProperties().setDelay(timeout);
return message;
}
}
);
}
}
- 消费消息
@Component
public class Consumer {
@RabbitListener(queues = {"delayedQueue"})
public void consume(String msg, Channel channel, Message message){
System.out.println("消费者收到延迟消息:" + msg);
try {
// 配置文件开启了手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
八、RabbitMQ的集群
RabbitMQ的镜像模式
RabbitMQ的集群
高可用
提升RabbitMQ的效率
搭建RabbitMQ集群
-
准备两台虚拟机(克隆)
-
准备RabbitMQ的yml文件启动效果
rabbitmq1:
version: '3.1' services: rabbitmq1: image: rabbitmq:3.8.5-management-alpine container_name: rabbitmq1 hostname: rabbitmq1 extra_hosts: - "rabbitmq1:192.168.11.32" - "rabbitmq2:192.168.11.33" environment: - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS ports: - 5672:5672 - 15672:15672 - 4369:4369 - 25672:25672
rabbitmq2:
version: '3.1' services: rabbitmq2: image: rabbitmq:3.8.5-management-alpine container_name: rabbitmq2 hostname: rabbitmq2 extra_hosts: - "rabbitmq1:192.168.11.32" - "rabbitmq2:192.168.11.33" environment: - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS ports: - 5672:5672 - 15672:15672 - 4369:4369 - 25672:25672
准备完毕之后,启动两台RabbitMQ
-
让RabbitMQ服务实现join操作执行成功后
需要四个命令完成join操作
让rabbitmq2 join rabbitmq1,需要进入到rabbitmq2的容器内部,去执行下述命令
rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl join_cluster rabbit@rabbitmq1 rabbitmqctl start_app
执行成功后:
-
设置镜像模式镜像模式
在指定的RabbitMQ服务中设置好镜像策略即可
九、RabbitMQ其他内容
9.1 Headers类型Exchange
headers就是一个基于key-value的方式,让Exchange和Queue绑定的到一起的一种规则
相比Topic形式,可以采用的类型更丰富。
headers绑定方式
具体实现方式
package com.wang.headers;
import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class Publisher {
public static final String HEADER_EXCHANGE = "header_exchange";
public static final String HEADER_QUEUE = "header_queue";
@Test
public void publish()throws Exception{
//1. 获取连接对象
Connection connection = RabbitMQConnectionUtil.getConnection();
//2. 构建Channel
Channel channel = connection.createChannel();
//3. 构建交换机和队列并基于header的方式绑定
channel.exchangeDeclare(HEADER_EXCHANGE, BuiltinExchangeType.HEADERS);
channel.queueDeclare(HEADER_QUEUE,true,false,false,null);
Map<String,Object> args = new HashMap<>();
// 多个header的key-value只要可以匹配上一个就可以
// args.put("x-match","any");
// 多个header的key-value要求全部匹配上!
args.put("x-match","all");
args.put("name","jack");
args.put("age","23");
channel.queueBind(HEADER_QUEUE,HEADER_EXCHANGE,"",args);
//4. 发送消息
String msg = "header测试消息!";
Map<String, Object> headers = new HashMap<>();
headers.put("name","jac");
headers.put("age","2");
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.headers(headers)
.build();
channel.basicPublish(HEADER_EXCHANGE,"",props,msg.getBytes());
System.out.println("发送消息成功,header = " + headers);
}
}
思维导图
本文来自博客园,作者:icui4cu,转载请注明原文链接:https://www.cnblogs.com/icui4cu/p/18903958