在MQ中,当消息成为死信(Dead message)后,消息中间件可以将其从当前队列发送到另一个队列中,这个队列就是死信队列。而在RabbitMQ中,由于有交换机的概念,实际是将死信发送给了死信交换机(Dead Letter Exchange,简称DLX)。死信交换机和死信队列和普通的没有区别。
消息成为死信的情况:
死信队列:
死信交换机和普通交换机,只是名字不同,其余的都一样;就是说死信交换机绑定死信队列,普通交换机绑定普通队列。
并且普通队列需要在创建的同时绑定死信交换机和死信队列的路由关键字;就是说当普通队列的消息达到死信的条件时,会通过绑定的死信交换机根据绑定的死信队列路由关键字发送该普通队列的消息,这个就是成为死信的过程。
除了拒签成为死信的需要监听普通队列,其余俩个不需要到达消费者端就会成为死信。
死信会有专门的死信消费者去监听。
package com.jjy.rabbitproducer;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig4 {
private final String DEAD_EXCHANGE = "dead_exchange";
private final String DEAD_QUEUE = "dead_queue";
private final String NORMAL_EXCHANGE = "normal_exchange";
private final String NORMAL_QUEUE = "normal_queue";
// 死信交换机
@Bean(DEAD_EXCHANGE)
public Exchange deadExchange(){
return ExchangeBuilder
.topicExchange(DEAD_EXCHANGE)
.durable(true)
.build();
}
// 死信队列
@Bean(DEAD_QUEUE)
public Queue deadQueue(){
return QueueBuilder
.durable(DEAD_QUEUE)
.build();
}
// 死信交换机绑定死信队列
@Bean
public Binding bindDeadQueue(@Qualifier(DEAD_EXCHANGE) Exchange exchange, @Qualifier(DEAD_QUEUE)Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("dead_routing")
.noargs();
}
// 普通交换机
@Bean(NORMAL_EXCHANGE)
public Exchange normalExchange(){
return ExchangeBuilder
.topicExchange(NORMAL_EXCHANGE)
.durable(true)
.build();
}
// 普通队列
@Bean(NORMAL_QUEUE)
public Queue normalQueue(){
return QueueBuilder
.durable(NORMAL_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE) // 绑定死信交换机
.deadLetterRoutingKey("dead_routing") // 死信队列路由关键字
.ttl(10000) // 消息存活10s
.maxLength(10) // 队列最大长度为10
.build();
}
// 普通交换机绑定普通队列
@Bean
public Binding bindNormalQueue(@Qualifier(NORMAL_EXCHANGE) Exchange exchange,@Qualifier(NORMAL_QUEUE)Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("my_routing")
.noargs();
}
}
生产者发送消息
@Test
public void TestDlx(){
// 存活时间过期后变成死信
rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信");
// 超过队列长度后变成死信
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信");
}
// 消息拒签但不返回原队列后变成死信
rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信");
}
消费者拒收消息
package com.jjy.rabbitconsumer;
import com.rabbitmq.client.Channel;
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 DlxConsumer {
@RabbitListener(queues = "normal_queue")
public void listenMessage(Message message, Channel channel) throws IOException {
// 拒签消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,false);
}
}
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
例如:用户下单后,30分钟后查询订单状态,未支付则会取消订单
但RabbitMQ中并未提供延迟队列功能,我们可以使用死信队列实现延迟队列的效果。
spring:
rabbitmq:
host: 192.168.66.100
port: 5672
username: jjy
password: jjy
virtual-host: /
# 日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
创建队列和交换机
package com.jjy.yanchirabbitmqdemo;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig {
// 订单交换机和队列
private final String ORDER_EXCHANGE = "order_exchange";
private final String ORDER_QUEUE = "order_queue";
// 过期订单交换机和队列
private final String EXPIRE_EXCHANGE = "expire_exchange";
private final String EXPIRE_QUEUE = "expire_queue";
// 过期订单交换机
@Bean(EXPIRE_EXCHANGE)
public Exchange deadExchange(){
return ExchangeBuilder
.topicExchange(EXPIRE_EXCHANGE)
.durable(true)
.build();
}
// 过期订单队列
@Bean(EXPIRE_QUEUE)
public Queue deadQueue(){
return QueueBuilder
.durable(EXPIRE_QUEUE)
.build();
}
// 将过期订单队列绑定到交换机
@Bean
public Binding bindDeadQueue(@Qualifier(EXPIRE_EXCHANGE) Exchange exchange,@Qualifier(EXPIRE_QUEUE) Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("expire_routing")
.noargs();
}
// 订单交换机
@Bean(ORDER_EXCHANGE)
public Exchange normalExchange(){
return ExchangeBuilder
.topicExchange(ORDER_EXCHANGE)
.durable(true)
.build();
}
// 订单队列
@Bean(ORDER_QUEUE)
public Queue normalQueue(){
return QueueBuilder
.durable(ORDER_QUEUE)
.ttl(10000) // 存活时间为10s,模拟30min
.deadLetterExchange(EXPIRE_EXCHANGE) // 绑定死信交换机
.deadLetterRoutingKey("expire_routing") // 死信交换机的路由关键字
.build();
}
// 将订单队列绑定到交换机
@Bean
public Binding bindNormalQueue(@Qualifier(ORDER_EXCHANGE) Exchange exchange,@Qualifier(ORDER_QUEUE) Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("order_routing")
.noargs();
}
}
编写下单的控制器方法,下单后向订单交换机发送消息
@RestController
public class OrderController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/place/{orderId}")
public String placeOrder(@PathVariable String orderId){
System.out.println("处理订单数据");
//将订单id发送到订单队列
rabbitTemplate.convertAndSend("order_exchange","order_routing",orderId);
return "下单成功,修改库存";
}
}
编写监听死信队列的消费者
package com.jjy.yanchirabbitmqdemo.consumer;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ExpireOrderConsumer {
//监听过期订单队列
@RabbitListener(queues = "expire_queue")
public void listenMessage(String orderId){
System.out.println("查询"+orderId+"订单状态,如果已支付无需处理,未支付,回退库存");
}
@RabbitListener(queues = "delayed_queue")
public void listenMessage2(String orderId){
System.out.println("查询"+orderId+"号订单的状态,如果已支付则无需处理,如果未支付则需要回退库存");
}
}
在使用死信队列实现延迟队列时,会遇到一个问题:RabbitMQ只会移除队列顶端的过期消息,如果第一个消息的存活时长较长,而第二个消息的存活时长较短,则第二个消息并不会及时执行。
RabbitMQ虽然本身不能使用延迟队列,但官方提供了延迟队列插
件,安装后可直接使用延迟队列。
安装延迟队列插件
# 将插件放入RabbitMQ插件目录中
mv rabbitmq_delayed_message_exchange-
3.9.0.ez /usr/local/rabbitmq/plugins/
# 启用插件
rabbitmq-plugins enable
rabbitmq_delayed_message_exchange
#停止rabbitmq
rabbitmqctl stop
#启动rabbitmq
rabbitmq-server restart -detached
此时登录管控台可以看到交换机类型多了延迟消息
管控台地址(https://自己地址:15672)默认账号和密码都是 guest
此时交换机是x-delayed-message类型的;延迟队列就是说发送延迟交换机及其绑定延迟队列的路由关键字和MessagePostProcessor对象,MessagePostProcessor对象是用来设置延迟时长等属性的。
延迟队列只是创建延迟交换机时跟普通不同,其他基本相同。
执行流程就是浏览器通过url映射到Controller的@RequestMapping注解的value中,然后执行方法体,执行到发送MQ,然后结束方法体;MQ此时延迟方法体中设置的时长,再发送给消费端,消费端监听到再处理延迟队列的MQ。
使用延迟队列
package com.jjy.yanchirabbitmqdemo;
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;
@Configuration
public class RabbitmqConfig2 {
public final String DELAYED_EXCHANGE = "delayed_exchange";
public final String DELAYED_QUEUE = "delayed_queue";
//1.延迟交换机
@Bean(DELAYED_EXCHANGE)
public Exchange delayedExchange() {
// 创建自定义交换机
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "topic"); // topic类型的延迟交换机
return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
//2.延迟队列
@Bean(DELAYED_QUEUE)
public Queue delayedQueue() {
return QueueBuilder
.durable(DELAYED_QUEUE)
.build();
}
// 3.绑定
@Bean
public Binding bindingDelayedQueue(@Qualifier(DELAYED_QUEUE) Queue queue, @Qualifier(DELAYED_EXCHANGE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("order_routing").noargs();
}
}
编写下单的控制器方法
package com.jjy.yanchirabbitmqdemo.controller;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/place2/{orderId}")
public String placeOrder2(@PathVariable String orderId) {
System.out.println("处理订单数据...");
// 设置消息延迟时间为10秒
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(10000);
return message;
}
};
// 将订单id发送到订单队列
rabbitTemplate.convertAndSend("delayed_exchange", "order_routing", orderId, messagePostProcessor);
return "下单成功,修改库存";
}
}
编写消费者
package com.jjy.yanchirabbitmqdemo.consumer;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ExpireOrderConsumer {
//监听过期订单队列
@RabbitListener(queues = "expire_queue")
public void listenMessage(String orderId){
System.out.println("查询"+orderId+"订单状态,如果已支付无需处理,未支付,回退库存");
}
@RabbitListener(queues = "delayed_queue")
public void listenMessage2(String orderId){
System.out.println("查询"+orderId+"号订单的状态,如果已支付则无需处理,如果未支付则需要回退库存");
}
}
在生产环境中,当单台RabbitMQ服务器无法满足消息的吞吐量及
安全性要求时,需要搭建RabbitMQ集群。
设置两个RabbitMQ服务
# 关闭RabbitMQ服务
rabbitmqctl stop
# 设置服务一
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server
start -detached
# 设置服务二
RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener
[{port,15674}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start -detached
将两个服务设置到同一集群中
# 关闭服务2
rabbitmqctl -n rabbit2 stop_app
# 重新设置服务2
rabbitmqctl -n rabbit2 reset
# 将服务2加入服务1中
rabbitmqctl -n rabbit2 join_cluster rabbit1@localhost(自己的主机名)
# 启动服务2
rabbitmqctl -n rabbit2 start_app
搭建了集群后,虽然多个节点可以互相通信,但队列只保存在了一个节点中,如果该节点故障,则整个集群都将丢失消息。
# 关闭服务1
rabbitmqctl -n rabbit1 stop_app
此时我们需要引入镜像队列机制,它可以将队列消息复制到集群中的其他节点上。如果一个节点失效了,另一个节点上的镜像可以保证服务的可用性。
在管此时某个节点故障则不会影响整个集群。控台点击Admin—>Policies设置镜像队列
Name为镜像策略的名字;Pattern(模式)的值为^,即应用于所有交换机和队列, ^xxx则是应用到以xxx开头交换机和队列。
ha-mode=all则是镜像模式:所有,即将队列镜像到集群的所有服务器中。
ha-sync-mode: automatic设置为自动同步,即当某一服务器宕机,而有新的消息发送到来,当该服务器再次启动后,它已经拿不到该消息了,但是通过设置该参数可以自动同步消息。
无论是生产者还是消费者,只能连接一个RabbitMQ节点,而在我们使用RabbitMQ集群时,如果只连接一个RabbitMQ节点,会造成该节点的压力过大。我们需要平均的向每个RabbitMQ节点发送请求,此时需要一个负载均衡工具帮助我们分发请求,接下来使用Haproxy做负载均衡:
安装Haproxy:yum -y install haproxy
配置Haproxy:vim /etc/haproxy/haproxy.cfg
添加下面内容
# 以下为修改内容
defaults
# 修改为tcp
mode tcp
# 以下为添加内容
listen rabbitmq_cluster
# 对外暴露端口
bind 0.0.0.0:5672
mode tcp
balance roundrobin
# 代理RabbitMQ的端口
server node1 127.0.0.1:5673 check inter 5000 rise 2 fall 2
server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2
listen stats
# Haproxy控制台路径
bind 自己地址:8100
mode http
option httplog
stats enable
stats uri /rabbitmq-stats
stats refresh 5s
访问Haproxy控制台:http://自己地址/rabbitmq-stats
生产者连接Haproxy发送消息
// 生产者
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.66.100");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection conn = connectionFactory.newConnection();
Channel channel = conn.createChannel();
channel.queueDeclare("simple_queue", false, false, false, null);
channel.basicPublish("", "simple_queue", null, "hello!rabbitmq!".getBytes());
channel.close();
conn.close();
}
}
总结:
3. RabbitMQ搭建集群及其实现镜像队列、负载均衡
关于RabbitMQ的学习就告一段落了 ,下次就是学习第二个消息中间件RocketMQ