文章目录
1.RabbitMQ简介
RabbitMQ基础架构:
Broke: 就是RabbitMQ服务器,生产者和消费者连接的对象。
Virtual Host:理解为服务器上不同的账户,互相隔离。
Exchange:交换机,用于将用户的消息 按照 用户发送来的路由key 传递对应的队列中,注意其只负责转发消息。
Queue: 生产者发送消息最终都会到队列中,生产者可以直接绑定该队列,也可以通过交换机与队列之间的绑定,由交换机将消息传递到队列上,以供消费者拉取消费。
Binding: 队列和交换机之间的绑定规则。
Connection: 生产者和消费者 需要和 Broker 之间建立TCP连接。
channel:connection内部的建立的逻辑连接,可用于多线程,减少每次都要connection的消耗。
2.Rabbit快速入门
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
2.1 简单模式
在简单模式中,只有生成者、队列、消费者,生产者直接将消息发送到队列中,消费者拉取消息。
生产者代码:创建连接 -> 创建通道 -> 创建队列(已存在,则用已存在的队列)-> 向队列中发送消息 -> 释放资源。
public static void main(String[] args) throws Exception {
//1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2. 设置参数
factory.setHost("localhost"); // ip 默认值 localhost
factory.setPort(5672); // 端口 默认值 5672
factory.setVirtualHost("/"); // 虚拟机 默认值/
factory.setUsername("zsk"); // 用户名 默认 guest
factory.setPassword("123456"); // 密码 默认值 guest
//3. 创建连接 Connection
Connection connection = factory.newConnection();
//4. 创建Channel
Channel channel = connection.createChannel();
//5. 创建队列Queue
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1.queue: 队列名称
* 2.durable: 是否持久化,当mq重启之后,数据还在
* 3.exclusive:
* * 是否独占,只能有一个消费者监听这队列
* * 当connection关闭时,是否删除队列
* 4.autoDelete: 是否自动删除,当没有Consumer时,自动删除掉
* 5.arguments: 参数
*/
// 如果没有hello_world队列,则会创建,如果有则不会创建
channel.queueDeclare("hello_world", true, false, false, null);
//6. 发送消息
/**
* String exchange, String routingKey, AMQP.BasicProperties props, byte[] body
* 1.exchange: 交换机名称。 简单模式下交换机会使用默认的 ""
* 2.routingKey: 路由名称 (有hello_world队列就会绑定) -- 后面具体讲
* 3.props: 配置信息
* 4.body: 发送消息数据
*/
String body = "hello rabbitmq~~~~";
channel.basicPublish("", "hello_world", null, body.getBytes());
// 7.释放资源
channel.close();
connection.close();
}
生产者代码:创建连接 -> 创建通道 -> 创建队列 -> 接收消息
在实际开发中,一般通过监听者来实现接收消息,不用关闭资源。
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("zsk");
factory.setPassword("123456");
//3. 创建连接 Connection -- 与生产者Connection是不一样的
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//5. 创建队列Queue (生产者已经创建了,这里可以不用)
channel.queueDeclare("hello_world", true, false, false, null);
// 接收消息对象
Consumer consumer = new DefaultConsumer(channel){
/**
* 回调方法,当收到消息后,会自动执行该方法
* @param consumerTag 标识
* @param envelope 获取一些信息,交换机,路由key...
* @param properties 配置信息
* @param body 数据
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag: " + consumerTag);
System.out.println("Exchange: " + envelope.getExchange());
System.out.println("RoutingKey: " + envelope.getRoutingKey());
System.out.println("properties: " + properties);
System.out.println("body: " + new String(body)); // 字节数组转换为字符串
}
};
/** basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:1.queue: 队列名称
* 2.autoAck: 是否自动确认
* 3.callback: 回调对象
*/
channel.basicConsume("hello_world", true, consumer);
// 消费者一般不关闭资源
}
2.2 工作队列模式
本质和简单模式区别不大,无非是多启动几个客户端,然后这个客户端竞争这个队列中的消息。默认是轮流的一种算法。
优点:消费者多可以提高消费处理速度,如:短信服务器可以部署多个等。
代码上同,多复制几个客户端即可。
2.3 发布订阅模式
上面两种模式,同一个消息只会被一个消费者接收,这里可以每个消息可以被多个消费者消费了,同时这里需要用到交换机了。
发布订阅模式流程:消费者将消息发送给交换机,同时设置路由key为""(因为将消息发送到所有被绑定的队列上,不需要绑定规则),每个消费者拉取队列中消息即可。
每个消费者都可以获取消息的好处在于可以分别做不同的处理,如一个打印到控制台,另一个将消息保存到数据库等。
生产者代码:
public class Producer_PubSub {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("zsk");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
* exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
* 参数:
* 1.exchange: 交换机
* 2.type: 交换机类型
* DIRECT("direct"), :定向
* FANOUT("fanout"), :扇形(广播),发送消息到每一个与之绑定队列。
* TOPIC("topic"), :通配符的方式
* HEADERS("headers"); :参数匹配
* 3.durable:是否持久化
* 4.autoDelete:自动删除
* 5.internal:内部使用,一般false
* 6.arguments:参数
*/
String exchangeName = "test_fanout";
// 5.创建交换机
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
// 6.创建队列
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
// 7.绑定队列和交换机
/**
* queueBind(String queue, String exchange, String routingKey)
* 参数:
* 1.queue: 队列名称
* 2.exchange: 交换机名称
* 3.routingKey: 路由键,绑定规则
* 如果交换机的类型为fanout,routingKey设置为""
*/
channel.queueBind(queue1Name, exchangeName, "");
channel.queueBind(queue2Name, exchangeName, "");
String body = "日志信息:........";
// 8.发送消息
channel.basicPublish(exchangeName, "", null, body.getBytes());
// 9.释放资源
channel.close();
connection.close();
}
}
消费者1代码:
public class Consumer_PubSub1 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("zsk");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
/** basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:1.queue: 队列名称
* 2.autoAck: 是否自动确认
* 3.callback: 回调对象
*/
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
/**
* 回调方法,当收到消息后,会自动执行该方法
* @param consumerTag 标识
* @param envelope 获取一些信息,交换机,路由key...
* @param properties 配置信息
* @param body 数据
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body: " + new String(body)); // 字节数组转换为字符串
System.out.println("将日志信息打印到控制台.....");
}
};
channel.basicConsume(queue1Name, true, consumer);
}
}
消费者2代码:
Consumer consumer = new DefaultConsumer(channel){
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body: " + new String(body)); // 字节数组转换为字符串
System.out.println("将日志信息保存到数据库.....");
}
};
channel.basicConsume(queue2Name, true, consumer);
2.4 Routing路由模式
在发布订阅模式中,控制台可以打印出任何级别的消息,而数据库完全没有必要存 Info 级别的日志消息。
路由模式:交换机可以根据路由key,将消息发送到不同队列,从而可以确保特定消息做特定的事 。
生产者代码:
public class Producer_Routing {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("zsk");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
交换机类型: DIRECT("direct"):定向
*/
String exchangeName = "test_direct";
// 5.创建交换机
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);
// 6.创建队列
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
// 7.绑定队列和交换机
/**
* queueBind(String queue, String exchange, String routingKey)
* 参数:
* 1.queue: 队列名称
* 2.exchange: 交换机名称
* 3.routingKey: 路由键,绑定规则
* 如果交换机的类型为fanout,routingKey设置为""
*/
// 队列1绑定 error
channel.queueBind(queue1Name, exchangeName, "error");
// 队列2绑定 info error warning
channel.queueBind(queue2Name, exchangeName, "info");
channel.queueBind(queue2Name, exchangeName, "error");
channel.queueBind(queue2Name, exchangeName, "warning");
String body = "日志信息:........";
// 8.发送消息
channel.basicPublish(exchangeName, "info", null, body.getBytes());
// 9.释放资源
channel.close();
connection.close();
}
}
消费者代码:和之前没啥区别,就改一下队列名即可。
2.5 Topics通配符模式
与定向模式不同,路由key可以是一个通配符(.和#)、。
.表示可以是一个单词,即只能是一层。
#表示可以是多个单词之间用.隔开,0层或多层。
生产者代码:
public class Producer_Topics {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("zsk");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
* 参数: TOPIC("topic"), :通配符的方式
*/
String exchangeName = "test_topic";
// 5.创建交换机
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
// 6.创建队列
String queue1Name = "test_topic_queue1";
String queue2Name = "test_topic_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
// 7.绑定队列和交换机
// routing key 系统的名称.日志的级别。
// 需求: 所以error级别的日志存入数据库,所有order系统的日志存入数据库
channel.queueBind(queue1Name, exchangeName, "#.error");
channel.queueBind(queue1Name, exchangeName, "order.*");
// 队列2绑定 info error warning
channel.queueBind(queue2Name, exchangeName, "*.*");
String body = "日志信息:........";
// 8.发送消息
channel.basicPublish(exchangeName, "ord3.i432", null, body.getBytes());
// 9.释放资源
channel.close();
connection.close();
}
}
3.SpringBoot整合RabbitMQ
依赖 -> yml -> 配置类(交换机、队列、绑定) -> 测试类(注入RabbitTemplete,发送消息)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: localhost # ip
ip: 5672
username: guest
password: guest
virtual-host: /
3.1 生产者
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "boot_topic_exchange";
public static final String QUEUE_NAME = "boot_queue";
// 1.交换机
@Bean("bootExchange")
public Exchange bootExchange() {
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
// 2.Queue队列
@Bean("bootQueue")
public Queue bootQueue() {
return QueueBuilder.durable(QUEUE_NAME).build();
}
// 3.队列和交换机绑定关系 Binding
/**
* 1.知道哪个队列
* 2.知道哪个交换机
* 3.routing key
*/
@Bean
public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,@Qualifier("bootExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
}
}
//测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class ReggieApplicationTest {
// 1.注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSend() {
// 发送的交换机 路由key 消息
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.haha", "boot mq hello");
}
}
3.2消费者
定义监听类(使用@RabbitListener注解完成队列监听)
@Component
public class RabbitMQListener {
@RabbitListener(queues = "boot_queue")
public void ListenerQueue(Message message) {
// System.out.println(message);
System.out.println(new String(message.getBody()));
}
}
4.RabbitMQ高级特性
4.1 消息的可靠性
生产者端:通过确认和回退机制确保投递消息的可靠性。
消费端:通过确认机制确保接收处理消息。
4.1.1 生产者之确认机制
confirm模式:发送者在发送消息时设置confirm回调监听,到达或不到达交换机都会调用。
spring:
rabbitmq:
host: localhost # ip
ip: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated # 开启确认模式
// 配置交换机和队列和binding
public static final String EXCHANGE_NAME1 = "testExchange";
public static final String QUEUE_NAME1 = "testQueue";
@Bean("testExchange")
public Exchange testExchange() {
return ExchangeBuilder.topicExchange(EXCHANGE_NAME1).durable(true).build();
}
@Bean("testQueue")
public Queue testQueue() {
return QueueBuilder.durable(QUEUE_NAME1).build();
}
@Bean
public Binding bindQueue2Exchange(@Qualifier("testQueue") Queue queue,@Qualifier("testExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
/* 测试
确认模式:
1.确认模式开启:publisher-confirm-type: correlated
2.在rabbitTemplate定义ConfirmCallBack回调函数
*/
@Test
public void testConfirm() {
// 2.定义回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 相关配置信息
* @param b exchange交换机 是否成功收到了消息。true 成功, false 失败
* @param s 失败原因 (交换机名字错了、路由失败等)
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("confirm方法被执行了....");
if (b) {
// 接收成功
System.out.println("接收成功消息" + s);
} else {
// 接收失败
System.out.println("接收失败原因" + s);
// 做一些处理,让消息再次发送
}
}
});
// 3.发送消息 (上面回调函数被自动执行了...)
rabbitTemplate.convertAndSend("testExchange", "confirm", "message confirm...");
}
4.1.2 生产者之退回模式
Exchange路由到Queue失败时 才会执行 ReturnCallBack
spring:
rabbitmq:
host: localhost # ip
ip: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated # 开启
publisher-returns: true #开启
/**
* 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack
* 步骤:
* 1. 开启回退模式:publisher-returns: true
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息的模式:
* 1.如果消息没有路由到Queue,则丢弃消息(默认)
* 2.如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn() {
// 设置交换机处理失败消息模式 (设置为true后,会把消息返回给生产者)
rabbitTemplate.setMandatory(true);
// 2.设置ReturnCallBack
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* Message message, int replyCode, String replyText, String exchange, String routingKey
* 1.message 消息对象
* 2.replyCode 错误码
* 3.replyText 错误消息
* 4.exchange 交换机
* 5.routingKey 路由键
* @param returnedMessage 上面信息被包装成对象了
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("return 执行了");
System.out.println(returnedMessage.getMessage()); // (Body:'message confirm...' ..)
System.out.println(returnedMessage.getReplyCode()); // 312
System.out.println(returnedMessage.getReplyText()); // NO_ROUTE
System.out.println(returnedMessage.getExchange()); // testExchange
System.out.println(returnedMessage.getRoutingKey());// confirm45
// 处理
}
});
// 3.发送消息 (这里路由key填个错的)
rabbitTemplate.convertAndSend("testExchange", "confirm45", "message confirm...");
}
测试:return 和 confirm 方法可以同时使用
4.1.3 消费者之确认模式
■ 自动确认:acknowledge=“none” – 消费者收到消息会自动给brake发送回执签收
■ 手动确认:acknowledge=“manual” – 消费者可以处理完消息手动调用代码发送签收
■ 根据移除情况确认:acknowledge=“auto” (使用麻烦,不作讲解)
spring:
rabbitmq:
host: localhost # ip
ip: 5672
username: guest
password: guest
virtual-host: /
listener:
direct:
acknowledge-mode: manual # 手动签收
/**
* Consumer ACK机制:
* 1.设置手动签收
* 2.Spring中让监听器类实现ChannelAwareMessageListener接口, SpringBoot 加个channel参数
* 3.如果消息处理成功,则调用 channel 的 basicAck() 签收
* 4.如果消息处理失败,则调用 channel 的 basicNack() 拒绝签收,broker重新发送给consumer
*/
@Component
public class RabbitMQListener {
@RabbitListener(queues = "testQueue")
public void ListenerTestQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签
try {
// 1.接收转换消息
System.out.println(new String(message.getBody()));
// 2.处理业务逻辑
System.out.println("处理业务逻辑...");
// int i = 3/0; // 出错了,broke一直重发
// 3.手动签收
channel.basicAck(deliveryTag, true); // true可以签收多条消息(之前的消息)
} catch (Exception e) {
// 4.拒绝签收
// 第三个参数: requeue 重回队列如果设置为true,则消息重新回到queue ,broker会重新发送该消息给消费端
channel.basicNack(deliveryTag, true, true);
// channel.basicReject(deliveryTag, true); // 单条处理,推荐使用上面的
}
}
}
4.1.4 可靠性总结
1.持久化 : exchange要持久化 queue要持久化 message要持久化
2.生产方确认 Confirm
3.消费方确认 Ack
4.Broker高可用
4.2 消费端限流
spring:
rabbitmq:
host: localhost # ip
ip: 5672
username: guest
password: guest
virtual-host: /
# publisher-confirm-type: correlated # 开启
# publisher-returns: true #开启
listener:
direct:
acknowledge-mode: manual # 一定要配置为手动确认
consumers-per-queue: 1 # 限制消费者一次拉取消息个数
/**
* Consumer限流机制
* 1.确保ack机制为手动确认。
* 2.Spring中listener-container配置属性 boot: consumers-per-queue: 1
* prefetch = 1000, 消费者每次从mq拉取1000条消息来消费,直到手动确认消费完毕后,才会继续拉取下一批消息
*/
@Component
public class QoSListener {
@RabbitListener(queues = "testQueue")
public void ListenerTestQueue(Message message, Channel channel) throws Exception {
//1. 获取消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
//3. 签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
}
}
4.3 TTL
设置消息的存活时间,到达指定时间,没有被消费,该消息就会被清除。
如果队列设置了TTL,到达指定时间,就会清除全部消息。
应用场景:
参数不用记,控制台都有
@Bean("test_exchange_ttl")
public Exchange testExchangeTTL() {
return ExchangeBuilder.topicExchange(EXCHANGE_NAME2).durable(true).build();
}
@Bean("test_queue_ttl")
public Queue testQueueTTL() {
// todo: ttl = 100000 队列统一时间过期
// return QueueBuilder.durable(QUEUE_NAME2).withArgument("x-message-ttl", 10000).build();
return QueueBuilder.durable(QUEUE_NAME2).ttl(10000).build();
}
@Bean
public Binding bindQueueExchangeTTL(@Qualifier("test_queue_ttl") Queue queue, @Qualifier("test_exchange_ttl") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("ttl.#").noargs();
}
/**
* TTL:过期时间
* 1.队列统一过期
*
* 2.消息单独过期
*
* 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
* 队列过期后,会将队列所有消息全部移除。
* 消息过期:只有消息在队列顶端,才会判断其是否过期(移除掉) -- 否则遍历队列一个个判断性能太差
*/
@Test
public void testTtl() {
/* for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hh", "message ttl..." + i);
}*/
// 消息的后处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 1.设置message的信息
message.getMessageProperties().setExpiration("5000"); // 消息的过期时间
// 2.返回该消息
return message;
}
};
// 消息单独过期
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hh", "message ttl...", messagePostProcessor);
}
4.4 死信队列
死信队列和死信交换机,用于处理过期的消息。
给队列设置参数:x-dead-letter-exchange和x-dead-letter-routing-key
消息成为死信的三种情况:
○ 1.队列消息长度到达限制;
○ 2.消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
○ 3.原队列存在消息过期设置。消息到达超时时间未被消息;
/**
* 死信队列:
* 1.声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)
* 2.声明死信队列(queue_dlx)和死信交换机(exchange_dlx)
* 3.正常队列绑定死信交换机
* 设置两个参数:
* * x-dead-letter-exchange: 死信交换机名称
* * x-dead-letter-routing-key: 发送给死信交换机的routingKey
*/
// 声明正常队列
@Bean("test_queue_dlx")
public Queue testQueueTestDlx() {
// 正常队列绑定死信交换机 这里 key 可以绑定即可 dlx.hehe
return QueueBuilder.durable("test_queue_dlx")
.withArgument("x-dead-letter-exchange", "exchange_dlx")
.withArgument("x-dead-letter-routing-key", "dlx.hehe")
.ttl(10000) // 队列过期时间
.maxLength(10) // 队列的长度限制
.build();
}
@Bean("test_exchange_dlx")
public Exchange testExchangeTestDlx() {
return ExchangeBuilder.topicExchange("test_exchange_dlx").durable(true).build();
}
@Bean
public Binding bindQueueExchangeTestDlx(@Qualifier("test_queue_dlx") Queue queue, @Qualifier("test_exchange_dlx") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("test.dlx.#").noargs();
}
// 声明死信队列
@Bean("queue_dlx")
public Queue testQueueDlx() {
return QueueBuilder.durable("queue_dlx").build();
}
@Bean("exchange_dlx")
public Exchange testExchangeDlx() {
return ExchangeBuilder.topicExchange("exchange_dlx").durable(true).build();
}
@Bean
public Binding bindQueueExchangeDlx(@Qualifier("queue_dlx") Queue queue, @Qualifier("exchange_dlx") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
测试结果需要到管理控制台看
/**
* 发送测试死信消息:
* 1.过期时间
* 2.长度限制
* 3.消息拒收
*/
@Test
public void testDlx() {
// 1.测试过期时间,死信消息
// rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");
// 2.测试长度限制后,消息死信
/* for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");
}*/
// 3.测试消息拒收
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "我是一条消息,进死信队列");
}
// 消费端拒收消息
@RabbitListener(queues = "test_queue_dlx")
public void ListenerTestQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签
try {
// 1.接收转换消息
System.out.println(new String(message.getBody()));
// 2.处理业务逻辑
System.out.println("处理业务逻辑...");
int i = 3/0; // 出错了,broke一直重发
// 3.手动签收
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
// 4.拒绝签收
System.out.println("拒绝接收");
// 第三个参数: requeue 不重回队列
channel.basicNack(deliveryTag, true, false);
}
}
4.5 延迟队列 – 重点
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求:
1.下单后,30分钟未支付,取消订单,回滚库存。
2.新用户注册成功7天后,发送短信问候。
实现方式:
1.定时器 [下单之后存到数据库表中,然后每隔一段时间判断用户是否付款,超时回滚]
2.延迟队列
在RabbitMQ中使用TTL+死信队列组合实现延迟队列的效果。
/**
* 延迟队列:
* 1.定义正常交换机(order_exchange)和队列(order_queue)
* 2.定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
* 3.绑定,设置正常队列过期时间为30分钟
*/
// 声明正常队列
@Bean("order_queue")
public Queue testQueueOrder() {
// 正常队列绑定死信交换机
return QueueBuilder.durable("order_queue")
.withArgument("x-dead-letter-exchange", "order_exchange_dlx")
.withArgument("x-dead-letter-routing-key", "dlx.order.cancel")
.ttl(10000) // 队列过期时间
.maxLength(10) // 队列的长度限制
.build();
}
@Bean("order_exchange")
public Exchange testExchangeOrder() {
return ExchangeBuilder.topicExchange("order_exchange").durable(true).build();
}
@Bean
public Binding bindQueueExchangeOrder(@Qualifier("order_queue") Queue queue, @Qualifier("order_exchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("order.#").noargs();
}
// 声明死信队列
@Bean("order_queue_dlx")
public Queue testQueueOrderDlx() {
return QueueBuilder.durable("order_queue_dlx").build();
}
@Bean("order_exchange_dlx")
public Exchange testExchangeOrderDlx() {
return ExchangeBuilder.topicExchange("order_exchange_dlx").durable(true).build();
}
@Bean
public Binding bindQueueExchangeOrderDlx(@Qualifier("order_queue_dlx") Queue queue, @Qualifier("order_exchange_dlx") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("dlx.order.#").noargs();
}
@Component
public class OrderListener {
@RabbitListener(queues = "order_queue_dlx") // 死信队列
public void ListenerTestQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息标签
try {
// 1.接收转换消息
System.out.println(new String(message.getBody()));
// 2.处理业务逻辑
System.out.println("处理业务逻辑...");
System.out.println("根据订单id查询其状态...");
System.out.println("判断状态是否为支付成功");
System.out.println("取消订单,回滚库存...");
// 3.手动签收
channel.basicAck(deliveryTag, true); // true可以签收多条消息(之前的消息)
} catch (Exception e) {
// 4.拒绝签收
System.out.println("拒绝签收");
channel.basicNack(deliveryTag, true, false);
}
}
}
@Test
public void testDelay() throws Exception {
//1.发送订单信息。 将来是在订单系统中,下单成功后,发送消息
rabbitTemplate.convertAndSend("order_exchange", "order.msg", "订单信息: id=1,time=2023年4月3日19:47:11");
//2.打印倒计时10s 10s之后死信队列才处理消息
for (int i = 10; i > 0; i--) {
System.out.println(i + "...");
Thread.sleep(1000);
}
}
4.6 日志监控 和 消息追踪
之后慢慢了解
5 RabbitMQ应用问题
5.1 消息可靠性
主要通过延迟发送第二次消息,防止第一次消息发送失败。
5.2 消息幂等性
幂等性:一次和多次请求某一个资源,对于资源本身应该具有同样的结果。
在MQ中,指多个消费者消费相同消息,最后结果相同(如数据库库存等)
通过乐观锁实现,增加一个version字段。
6 RabbitMQ集群搭建
单机多实例
service rabbitmq-server stop # 停止服务,因为单机上要通过进程号区分
# 启动第1个实例 rabbit1监听5673 其管理控制台进程号是 15672 默认的
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start
# 启动第2个实例 其管理控制台进程号是 15674
RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start
# rabbitmqctl -n rabbit1 stop # 停止对应服务
# rabbitmqctl -n rabbit2 stop
rabbit1设置为主节点
rabbitmqctl -n rabbit1 stop_app
rabbitmqctl -n rabbit1 reset
rabbitmqctl -n rabbit1 start_app
rabbit2设置为从节点
rabbitmqctl -n rabbit2 stop_app
rabbitmqctl -n rabbit2 reset
rabbitmqctl -n rabbit2 join_cluster rabbit1@'super' ###''内是super主机名换成自己的
rabbitmqctl -n rabbit2 start_app
rabbitmqctl cluster_status -n rabbit1 // 查看集群状态
此时并没有 主从同步,主节点数据没有同步到从节点上
RabbitMQ镜像集群配置
要复制队列内容到集群里的每个节点,必须要创建镜像队列。
– 这样每个节点消息进行了同步
但是这样存在一个问题,一个节点挂了,访问另一个节点需要修改端口,所以我们可以通过路由解决这个问题
可以通过负载均衡-HAProxy解决该问题