本文是基于B站-黑马程序员发布的RabbitMQ教程所记录(高级部分后续补上),仅供学习参考,如果需要更详细的文档,请移步官网哦
MQ全称Message Queue(消息队列),是在消息的传输过程中保存消息的容器,多用于分布式系统之间进行通信。
即:
应用解耦:使用MQ使得应用间解耦,提升容错性和可维护性。举个例子,如订单系统需要调用库存系统、支付系统、物流系统完成服务,如果通过远程调用,二者就耦合在一起了,如果使用MQ,订单系统将消息发送到MQ,其它系统去MQ中取出消息进行消费即可,大大降低了模块之间的耦合。且如果需要加入新系统,原订单系统也无需修改代码,只需要让新模块同样去MQ中取出订单系统的消息消费即可。
异步提速:使用MQ提升用户体验和系统吞吐量。举个例子,如用户下订单,根据传统的执行流程,首先需要花费300ms调用库存系统,再花费300ms调用支付系统,再花费300ms调用物流系统,最后花费20ms写入数据库。这样依赖就花费了大约1s时间,较为影响用户体验,因此,如果使用MQ,则用户下完订单,花费20ms写入数据库,花费5ms将消息发送至MQ中,然后返回成功消息,一共花费25ms,而其它系统自行取消息执行,时间不计入。这样依赖就大大降低了用户等待时间,提升了响应速度。
削峰填谷:使用MQ提高系统稳定性。举个例子,如某网站推出十二点准时一元抢家电活动,等到十二点,一瞬间有五千个请求涌入,如果直接用服务器去接收这些流量,服务器会瞬间宕机,严重影响用户体验。如果使用MQ,作为中间间缓存请求,即让请求首先访问的是MQ,而不是直接打到服务器,然后让服务器去按处理阈值去MQ中取请求进行处理,这样服务器就能正常工作,这就是MQ削峰,服务器填谷。
使用MQ既有优势也有劣势,使用MQ最好满足以下条件再考虑使用:
目前业界有很多的MQ产品,例如RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ等,当然,也有直接使用Redis充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及MQ产品特征,综合考虑。
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里巴巴 | Apache |
开发语言 | Erlang | Java | Java | Scale&Java |
协议支持 | AMOP、XMPP、SMTP、STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义 | 自定义协议,社区封装了HTTP协议支持 |
客户端支持语言 | 官方支持Erlang、Java、Ruby等,社区产出多种API,几乎支持所有语言 | Java,C,C++,Python,PHP,perl,.net等 | Java,C++(不成熟) | 官方支持Java,社区产出多种API,如PHP,Python |
单机吞吐量 | 万级(其次) | 万级(最差) | 十万级(最好) | 十万级(次之) |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
功能特性 | 并发能力强,性能极其好,延时低,社区活跃,管理界面丰富 | 老牌产品,成熟度高,文档较多 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,毕竟是为大数据领域准备的。 |
介绍RabbitMQ前,先介绍下AMQP协议,即Advanced Message Queuing Protocol(高级消息队列协议)是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计规范。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品、不同的开发语言等条件的限制,该协议规范于2006年发布。
在2007年,Rabbit公司基于AMQP标准开发的RabbitMQ1.0发布。RabbitMQ采用Erlang语言开发。架构如下:
其中的相关概念为:
RabbitMQ提供了六种工作模式:简单模式、work queues、Publish/Subscribe发布与订阅模式、Routing路由模式、Topic主题模式、RPC远程调用模式(远程调用,不太算MQ,不作介绍)。
使用 Java 代码测试一下RabbitMQ的简单模式。首先在项目中引入RabbitMQ的客户端依赖(操作RabbitMQ),并且RabbitMQ的服务要启动起来。
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.6.0version>
dependency>
然后根据MQ架构来编写生产者相关代码:
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建队列Queue
/*
* 队列创建方法: queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguements):没有该队列时自动创建该队列
* 参数:
* 1、queue: 队列名称
* 2、durable: 是否持久化,即当mq重启之后,其中的消息还在
* 3、exclusive: 是否独占,即是否只能有一个消费者监听这队列,当Connection关闭时,是否删除队列。
* 4、autoDelete: 是否自动删除。即当没有Consumer时,自动删除队列。
* 5、arguments: 删除的一些参数
*/
channel.queueDeclare("hello_world", true, false, false, null);
//6、发送消息
/*
* 消息发送方法: basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* 参数:
* 1、exchange: 交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey: 路由名称。
* 3、props: 配置信息
* 4、body: 发送消息数据
*
*/
channel.basicPublic("", "hello_world", null, "hello rabbitmq".getBytes());
//7、释放资源
channel.close();
connection.close();
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、接收消息
//创建回调对象在接收到消息后进行方法回调
Consumer consumer = new DefaultConsumer(channel){
/*
* 覆写回调方法,当受到消息后,会自动执行该方法
* 参数:
* 1、consumerTag: 唯一标识
* 2、envelope: 获取一些信息,交换机,路由Key...
* 3、properties: 配置信息
* 4、body: 消息数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body){
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);
//释放资源?不用
上面已经对简单模式进行了测试。不难看出,RabbitMQ的各种工作模式其实就是消息的路由策略和分发方式不一样。
Work queues工作队列模式相较于简单模式,只是在多了一个或多个消费者,这些消费者竞争同一个队列中的消息。对于任务较多情况或任务过重的情况下使用工作队列可以提高任务处理的速度。
Pub / Sub订阅模型中,多了一个Exchange角色(其实其它模式也有,只不过使用的是默认交换机),在使用时与简单模式和工作队列模式略有区别。
如上图中,简单介绍下:
P:生产者,也就是要发送消息的程序,但是不再直接发送消息到队列中,而是发给X(交换机)
C:消费者,消息的接收者,会一直监听队列等待消息。
Queue:消息队列,接收消息,缓存消息。
Exchange:交换机(X),一方面,接收生产者发送的消息。另一方面,直到如何处理消息,例如递交给某个特定队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange常见类型有以下三种类型:
Exchange(交换机)只负责转发消息,并不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会被丢失。
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*
* 创建交换机方法: exchangeDeclare(String exchange, BuiltExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map arguements)
* 参数:
* 1、exchange: 交换机名称
* 2、type: 交换机类型(枚举)
* DIRECT("direct"): 定向
* FANOUT("fanout"): 扇形(广播),发送消息到每一个与之绑定队列
* TOPIC("topic"): 通配符的方式
* HEADERS("headers"): 参数匹配
* 3、durable: 是否持久化
* 4、autoDelete: 自动删除
* 5、internal: 内部使用。一般为false。
* 6、arguments: 参数
*/
channel.exchangeDeclare("test_fanout", BuiltExchangeType.FANOUT, true, false, false, null);
//6、创建队列Queue
/*
* 队列创建方法: queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguements):没有该队列时自动创建该队列
* 参数:
* 1、queue: 队列名称
* 2、durable: 是否持久化,即当mq重启之后,其中的消息还在
* 3、exclusive: 是否独占,即是否只能有一个消费者监听这队列,当Connection关闭时,是否删除队列。
* 4、autoDelete: 是否自动删除。即当没有Consumer时,自动删除队列。
* 5、arguments: 删除的一些参数
*/
channel.queueDeclare("test_fanout_queue1", true, false, false, null);
channel.queueDeclare("test_fanout_queue2", true, false, false, null);
//7、绑定队列和交换机
/*
* 绑定方法: queueBind(String queue, String exchange, String routingKey)
* 参数:
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey: 路由键,即绑定规则。如果交换机的类型为fanout,routingKey无论怎么设置都会给绑定的队列发送消息。
*/
channel.queueBind("test_fanout_queue1", "test_fanout", "");
channel.queueBind("test_fanout_queue2", "test_fanout", "");
//6、发送消息
/*
* 消息发送方法: basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* 参数:
* 1、exchange: 交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey: 路由名称。
* 3、props: 配置信息
* 4、body: 发送消息数据
*
*/
channel.basicPublic("test_fanout", "", null, "hello rabbitmq".getBytes());
//7、释放资源
channel.close();
connection.close();
多个消费者的话监听不同队列即可。
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body){
System.out.println("body:" + new String(body));
System.out.println("将数据保存到数据库...");
}
}
channel.basicConsume("test_fanout_queue1", true, consumer);
//释放资源?不用
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body){
System.out.println("body:" + new String(body));
System.out.println("将消息日志打印到控制台...");
}
}
channel.basicConsume("test_fanout_queue2", true, consumer);
//释放资源?不用
Routing工作模式是基于routingKey来工作的,比如在发送消息到交换机前要指定routingKey,然后交换机通过routingKey发送到不同的队列中(队列在创建时可以指定自己的routingKey),让不同消费者消费。
根据发送的 routingKey发送到指定routingKey的队列中。
生产者:
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*
* 创建交换机方法: exchangeDeclare(String exchange, BuiltExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map arguements)
* 参数:
* 1、exchange: 交换机名称
* 2、type: 交换机类型(枚举)
* DIRECT("direct"): 定向
* FANOUT("fanout"): 扇形(广播),发送消息到每一个与之绑定队列
* TOPIC("topic"): 通配符的方式
* HEADERS("headers"): 参数匹配
* 3、durable: 是否持久化
* 4、autoDelete: 自动删除
* 5、internal: 内部使用。一般为false。
* 6、arguments: 参数
*/
channel.exchangeDeclare("test_direct", BuiltExchangeType.DIRECT, true, false, false, null);
//6、创建队列Queue
/*
* 队列创建方法: queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguements):没有该队列时自动创建该队列
* 参数:
* 1、queue: 队列名称
* 2、durable: 是否持久化,即当mq重启之后,其中的消息还在
* 3、exclusive: 是否独占,即是否只能有一个消费者监听这队列,当Connection关闭时,是否删除队列。
* 4、autoDelete: 是否自动删除。即当没有Consumer时,自动删除队列。
* 5、arguments: 删除的一些参数
*/
channel.queueDeclare("test_direct_queue1", true, false, false, null);
channel.queueDeclare("test_direct_queue2", true, false, false, null);
//7、绑定队列和交换机
/*
* 绑定方法: queueBind(String queue, String exchange, String routingKey)
* 参数:
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey: 路由键,即绑定规则。如果交换机的类型为fanout,routingKey无论怎么设置都会给绑定的队列发送消息。
*/
//queue1和direct交换机绑定routingKey为error
channel.queueBind("test_direct_queue1", "test_direct", "error");
//queue2和direct交换机绑定routingKey为info,error,warning
channel.queueBind("test_direct_queue2", "test_direct", "info");
channel.queueBind("test_direct_queue2", "test_direct", "error");
channel.queueBind("test_direct_queue2", "test_direct", "warning");
//6、发送消息
/*
* 消息发送方法: basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* 参数:
* 1、exchange: 交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey: 路由名称。
* 3、props: 配置信息
* 4、body: 发送消息数据
*
*/
channel.basicPublic("test_direct", "error", null, "error rabbitmq".getBytes());
channel.basicPublic("test_direct", "info", null, "info rabbitmq".getBytes());
//7、释放资源
channel.close();
connection.close();
通过Topic通配符方式绑定队列,规则是队列1定义自己的routingKey为a.b.c。Topic交换机通过通配符绑定队列,即通过***,#的方式进行匹配,表示不多不少只匹配一个单词,而#表示能够匹配一个或多个单词*。
生产者:
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("192.168.xx.xx");//ip地址 默认值为Localhost(不设置情况下)
factory.setPort(5672);//端口 默认值为5672
factory.setVirtualHost("/");//设置虚拟机 默认为/
factory.setUsername("guest");//用户名 默认为guest
factory.setPassword("guest");//密码 默认为guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*
* 创建交换机方法: exchangeDeclare(String exchange, BuiltExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map arguements)
* 参数:
* 1、exchange: 交换机名称
* 2、type: 交换机类型(枚举)
* DIRECT("direct"): 定向
* FANOUT("fanout"): 扇形(广播),发送消息到每一个与之绑定队列
* TOPIC("topic"): 通配符的方式
* HEADERS("headers"): 参数匹配
* 3、durable: 是否持久化
* 4、autoDelete: 自动删除
* 5、internal: 内部使用。一般为false。
* 6、arguments: 参数
*/
channel.exchangeDeclare("test_topic", BuiltExchangeType.TOPIC, true, false, false, null);
//6、创建队列Queue
/*
* 队列创建方法: queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguements):没有该队列时自动创建该队列
* 参数:
* 1、queue: 队列名称
* 2、durable: 是否持久化,即当mq重启之后,其中的消息还在
* 3、exclusive: 是否独占,即是否只能有一个消费者监听这队列,当Connection关闭时,是否删除队列。
* 4、autoDelete: 是否自动删除。即当没有Consumer时,自动删除队列。
* 5、arguments: 删除的一些参数
*/
channel.queueDeclare("test_topic_queue1", true, false, false, null);
channel.queueDeclare("test_topic_queue2", true, false, false, null);
//7、绑定队列和交换机
/*
* 绑定方法: queueBind(String queue, String exchange, String routingKey)
* 参数:
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey: 路由键,即绑定规则。如果交换机的类型为fanout,routingKey无论怎么设置都会给绑定的队列发送消息。
*/
//routingKey规定格式为: 系统名称.日志级别
//需求1: 所有error级别,order系统的日志入队列1
channel.queueBind("test_topic_queue1", "test_topic", "#.error");
channel.queueBind("test_topic_queue1", "test_topic", "order.*");
//需求2: 所有日志信息发送到队列2
channel.queueBind("test_topic_queue2", "test_topic", "*.*");
//6、发送消息
/*
* 消息发送方法: basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* 参数:
* 1、exchange: 交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey: 路由名称。
* 3、props: 配置信息
* 4、body: 发送消息数据
*
*/
channel.basicPublic("test_topic", "error", null, "error rabbitmq".getBytes());
channel.basicPublic("test_direct", "info", null, "info rabbitmq".getBytes());
//7、释放资源
channel.close();
connection.close();
生产者:
消费者:
Spring方式配置整合(SpringBoot则按照其指定的名字进行配置):
rabbitmq.host=192.168.xx.xx
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.username=guest
rabbit.virtual-host=/
发送消息则通过RabbitTemplate进行发送,前提是对应交换机、队列(已设置队列路由)创建完成。且容器中存在ConnectionFactory,连接工厂需要配置好host、port、username、password、virtual-host。
接收消息则创建一个类实现MessageListener接口并重写其中的onMessage()方法,并将该类绑定为监听某个队列并加入容器。
生产者:
创建生产者SpringBoot工程
引入依赖坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
编写yml配置,基本信息配置(host、port、username、password、virtual-host)
spring:
rabbitmq:
host: xxx.xxx.xxx.xxx
port: 5672
username: guest
password: guest
virtual-host: /
定义交换机,队列以及绑定关系的配置类
@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、队列
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
//3、交换机和队列绑定关系 Binding
/**
* 1、队列
* 2、交换机
* 3、routingKey
*/
@Bean
public Binding bindQueueExchange(@Qualifier("bootExchange") Exchange exchange,
@Qualifier("bootQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
}
}
注入RabbitTemplate,调用方法,完场消息发送
@Autowired
public RabbitTemplate rabbitTemplate;
@Test
public void testRabbitMQ(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,
"boot.log", "log发送...".getBytes(StandardCharsets.UTF_8));
}
消费者代码:
@Component
public class MQConsumer1{
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void ListenerQueue(Message message){
System.out.println("监听到消息: " + new String(message.getBody()));
}
}
小结:
消息可靠性投递需要区分消息的经过路段,生产者到交换机,交换机是否宕机,消费者是否宕机,这些都影响消息投递的可靠性。
而通过以下四点,基本可以保证消息投递的可靠性:
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ的过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否成功到达交换机,或是否到达队列。
当然,为了区分不同消息,在发送消息前需要给每个消息设置一个全局唯一id,以区分不同消息,避免ack冲突。
1、使用SpringBoot来实现MQ确认机制,首先先引入依赖,配置好基本信息配置,再添加生产者确认的相关配置:
spring:
rabbitmq:
host: xx.xx.xx.xxx
port: 5672
username: guest
password: guest
virtual-host: /
#开启消息投递确认的异步回调
publisher-confirm-type: correlated
#开启消息投递回执的回调
publisher-returns: true
#路由失败策略
template:
mandatory: true
常用配置说明:
**2、**根据以上的配置来配置回调方法,先配置ReturnCallback,触发ReturnCallback说明消息到达了交换机,但未路由到队列。
由于RabbitTemplate交由Spring管理,因此该RabbitTemplate为单例,而一个RabbitTemplate只能配置一个ReturnCallback,因此需要考虑RabbitTemplate设置ReturnCallback的时机,最好创建一个Bean实现容器通知方法,然后在容器启动时拿到RabbitTemplate来设置。
@Configuration
@Slf4j
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) ->{
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
}
}
3、然后就是ConfirmCallback回调,这种回调每次发送消息都可以设置,并不是每个RabbitTemplate只有一个,ConfirmCallback回调的情况分为三种:ACK,NACK,无。ACK表示投递成功,NACK表示投递失败,无表示代码可能抛出了异常。代码为:
@Test
public void test3(){
//消息体
String message = "hello, spring amqp";
//消息ID,需要封装到CorrelationData中,因为我们设置的投递确认模式为correlated
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//添加callback
correlationData.getFuture().addCallback(
result -> {
if (result.isAck()){
//ack,成功投递
log.debug("消息发送成功,ID:{}",correlationData.getId());
}else{
//nack,投递失败
log.error("消息发送失败,ID:{},原因:{}",correlationData.getId(), result.getReason());
}
},
ex -> {
//出现异常
log.error("消息发送异常,ID:{},原因:{}", correlationData.getId(), ex.getMessage());
}
);
//发送消息
rabbitTemplate.convertAndSend("amq.direct","simple", message, correlationData);
}
总结在SpringAMQP中处理消息确认的几种情况:
当然,如果出现失败或者异常或者ReturnCallback的情况,可以进行重试,并且至少要有一个兜底方案。
在生产者处保证了消息的可靠投递,那么在Broker处如果出现如机器宕机的情况,那么仍可能出现消息丢失问题,这时就需要考虑消息在Broker处的持久化问题。
通过代码配置交换机、队列和消息的持久化:
交换机:
//1、交换机
@Bean("bootExchange")
public Exchange bootExchange(){
//return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
//三个参数:交换机名称,是否持久化,是否自动删除(交换机绑定的队列全部被删除后是否自动删除交换机)
return new TopicExchange(RabbitMQConfig.EXCHANGE_NAME,true, false);
}
队列:
//2、队列
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
消息是默认持久化的,如果仍需要配置,则使用Message类型进行配置,然后将Message作为消息发送即可:
Message message = MessageBuilder.withBody("hello, spring amqp".getBytes(StandardCharsets.UTF_8))//消息体
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.build();
其实在使用SpringAMQP且不手动设置情况下,底层方法对交换机、队列、消息都是默认持久化的。
在保证了消息投递可靠和消息持久化可靠后,压力就来到了消费者这边,如果消费者在消费消息时挂了,那么就无法保证消息的可靠性。
因此RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ受到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式:
消费者消息确认配置方式是修改application.yml文件,添加下面配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: none #none:关闭ack; manual:手动ack; auto:自动ack
MQ支持的这三种确认模式中,当确认模式为none时,消费者拿到了消息就向MQ发送ack,MQ就立刻删除队列中存储的消息。
当确认模式为auto时,消费者拿到消息且执行过程中没有产生异常,就返回ack,MQ就删除队列中的消息,但若消费者拿到消息执行过程中产生了异常,就返回nack,MQ就不删除消息,让消费者再次获取消息。
以上的auto消费者确认模式存在一些问题,当代码有问题时,每次拿到消息后进行执行都会抛出异常,然后每次都返回nack,然后又去队列中再次获取该消息,如此一直循环,将造成服务器较大压力。
因此需要消费失败重试机制,但该重试机制不能无限重试,因此我们可以利用Spring的retry机制,在消费者出现异常时利用Spring的限制重试,而不是无限制的requeue到mq队列。
spring:
rabbitmq:
host: xx.xx.xx.xxx
port: 5672
username: guest
password: guest
virtual-host: /
#开启消息投递确认的异步回调
publisher-confirm-type: correlated
#开启消息投递回执的回调
publisher-returns: true
#路由失败策略
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
prefetch: 1
#retry机制[注意:所有重试结束,消息无论是否被成功消费,都会被MQ丢弃(最后消费端返回reject)]
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 第一次失败等待时长
multiplier: 2 # 后续失败的等待时长倍率。即若倍率为2,第一次等1s再试,第二次则2s,第三次则4s
max-attempts: 3 #最大重试次数
stateless: true # 是否设置为无状态,true即无状态,false为有状态,作用即是否保持原状态,开启事务情况下要改为false,保持事务状态。
使用Spring的重试机制也存在消息丢失问题,即如果重试次数过多,耗尽重试次数,消息就会被丢弃而丢失(默认处理方案),在Spring中,消息重试次数耗尽后,处理策略其实是由MessageRecoverer接口来处理的,该接口包含三种不同的实现:
而使用重试次数耗尽后投递到指定交换机的方法能够彻底解决消息丢失问题。
然后测试下RepublishMessageRecoverer处理模式:
首先需要定义接收失败消息的交换机、队列及其绑定关系。
//直接交换机
@Bean
public DirectExchange errorMessageExchange(){
return ExchangeBuilder.directExchange("error.direct").build();
}
//持久化队列
@Bean
public Queue errorQueue(){
return QueueBuilder.durable("error.queue").build();
}
//绑定
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}
定义自己的失败重试策略,SpringBoot中默认配置的是RejectAndDontRequeueRecoverer,但是@ConditionOnMissingBean注解表明只要我们配置了MessageRecoverer接口的实现类,就不自动配置。
//RepublishMessageRecoverer策略
@Bean
public RepublishMessageRecoverer republishMessageRecoverer(){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
如何确保RabbitMQ消息投递的可靠性?
即Time-To-Live,如果一个队列中的消息TTL结束仍未被消费,则会变成死信,TTL超时分为两种超时:
样例:设置消息超时时间ttl=5000;设置队列超时时间x-message-ttl=10000
消费者消费死信队列代码:
/**
* 在声明消费者时声明死信交换机和死信队列即绑定
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true"),
exchange = @Exchange(name = "dl.direct"),
key = "dl"
))
public void listenDlQueue(String msg){
log.info("接收到 dl.queue的延迟消息:{}", msg);
}
创建正常交换机和队列:
@Bean
public DirectExchange ttlExchange(){
return new DirectExchange("ttl.direct");
}
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable("ttl-queue")//指定队列名称,并持久化
.ttl(10000) //设置队列的超时时间为10s
.deadLetterExchange("dl.direct")//指定死信交换机
.deadLetterRoutingKey("dl")//指定队列发送到交换机的routingKey
.build();
}
@Bean
public Binding simpleBinding(){
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}
如果要对消息本身设置存活时间,按如下方式即可:
@Test
public void test4(){
Message message = MessageBuilder.withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))//消息体
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.setExpiration("5000")//设置消息超时时间为5s
.build();
//为每条消息设置唯一的ID
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
log.info("消息发送成功!");
}
先谈谈死信,当一个队列中的消息满足下列情况之一时,就可以被称为死信(dead letter,也可以理解为即将被丢弃的消息):
队列如果配置了dead-letter-exchange属性,指定好一个交换机,那么队列中的死信就会由队列投递到这个交换机中,而这个交换机就被称为死信交换机(Dead Letter Exchange,简称DLX)。当然队列还可以配置与死信交换机绑定的死信队列的RoutingKey,可以通过dead-letter-routing-key属性来指定。
死信交换机的方式类似于RepublishMessageRecoverer策略,不过Republish是通过消费端将死信发送至交换机,而死信交换机是由队列来投递消息的,可以指定队列绑定的死信交换机。
让消息超时的两种设置方式是?
如何实现发送一个消息20秒后消费者才收到消息?
让消费者去监听死信队列,生产者设置消息过期时间为20s,然后发送到正常交换机中,并路由到正常队列(已绑定死信交换机和死信队列),等20s超时后由队列转发到死信交换机,再转到死信队列被消费者监听消费。
延迟队列的效果类似TTL + 死信队列,但没那么复杂。RabbitMQ官方使用了一个插件实现了延迟队列,首先需要在Linux中或Docker容器中进行安装:https://blog.csdn.net/DZP_dream/article/details/118391439
虽然被称作延迟队列,但起作用的其实是交换机,即x-delayed-message。在安装好后,可以在新增交换机位置处选择x-delayed-message类型的交换机。
使用该延迟交换机的步骤为:
延迟交换机受到带有Header消息的消息时,会将消息缓存指定时间(与其它交换机不同,交换机一般不存储消息),然后再根据路由方式发送到指定队列。
延迟交换机的声明有两种方式,一种是基于注解的方式,一种是基于Bean的方式:
基于注解:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayedQueue(String msg){
log.info("接收到 delay.queue的延迟消息:{}", msg);
}
基于Bean:
@Bean
public DirectExchange delayedExchange(){
return ExchangeBuilder.directExchange("delay.direct")
.delayed()//设置delayed属性为true
.durable(true)//持久化设置
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayedBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with("delay");
}
延迟队列插件的使用步骤包括哪些?
谈惰性队列前先谈消息堆积问题,当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,一直到队列存满,而最早入队的消息,就成了死信,就可能会被丢弃,这就是消息堆积带来的问题,因此消息堆积是不健康的现象。
解决消息堆积有三种思路:
因此就需要引出惰性队列了,RabbitMQ队列是基于内存的,速度快,但是有存储上限,一般到达内存限额的40%左右,RabbitMQ就会停止接收消息,先将一部分消息存入磁盘,降低内存使用,再接收消息,而消息堆积问题最容易引起这种情况,而导致RabbitMQ性能忽高忽低。
而惰性队列中基本解决了这个问题,因为惰性队列中的消息是存入磁盘而非内存中的,惰性队列有以下特征:
如果要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。或者直接通过Linux命令将一个运行中的队列修改为惰性队列。
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
而使用SpringAMQP声明惰性队列分为两种方式:
基于Bean:
@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy()//开启 x-queue-mode为Lazy
.build();
}
基于注解:
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){
log.info("接收到 lazy.queue 的消息:{}", msg);
}
消息堆积问题的解决方案?
惰性队列的优点?
惰性队列的缺点?
RabbitMQ基于Erlang语言编写,Erlang是一个面向并发的语言,天然支持集群模式。
RabbitMQ的集群有两种模式:
镜像集群虽然支持主从,但其主从并不是强一致的,在某些情况下(如备份时宕机)可能有数据丢失的风险。因此RabbitMQ在3.8版本后,推出新的功能:仲裁队列来代替镜像集群,底层采用Raft协议来确保主从数据一致性。
普通集群,或者叫标准集群(classic cluster),其具备以下特征:
镜像集群:本质是主从模式,但是具备以下特征:
镜像选择有三种模式:
ha-mode(模式) | ha-params | 效果 |
---|---|---|
准确模式exactly | 队列的副本量count | 集群中队列的副本数(包括主节点),count如果为1表示只有单个副本:即队列主节点。count为2表示2个副本:1主队列和1个镜像队列。换句话说:count = 镜像数量 + 1。如果集群中的节点数量小于count,则该队列将镜像到所有节点。如果有集群总数大于count + 1,且如果包含镜像的节点出现故障,则将在另一个节点上创建一个新的镜像。 |
all | (none) | 队列在集群中的所有节点之间镜像。队列将镜像到任何新加入的节点。镜像到所有节点会对所有集群节点施加额外的压力,如网络IO,磁盘IO,推荐使用exactly模式,设置副本数量为(n/2 + 1) |
nodes | node names | 指定队列创建到哪些具体节点,如果指定的系欸但不存在,则出现异常。 |
仲裁队列的出现主要是为了解决镜像队列数据丢失的问题,RabbitMQ3.8以后才有的新功能,用来代替镜像队列,具备以下特征:
仲裁队列的添加十分简单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O30YYAFk-1675922744941)(C:\Users\Lenovo\Desktop\note_pictures\image-20220902150821530.png)]
测试使用SpringAMQP代码声明仲裁队列:
首先如果是集群状态,要先通过SpringAMQP连接集群,需要在yaml中配置:
spring:
rabbitmq:
addresses: 192.168.150.xxx:8071, 192.168.150.xxx:8072, 192.168.150.xxx:8073
username: guest
password: guest
virtual-host: /
使用@Bean创建仲裁队列:
@Bean
public Queue quorumQueue(){
return QueueBuilder.durable("quorum.queue2")
.quorum()//设置为仲裁队列
.build();
}
|
| :-------------: | :---------------: | :----------------------------------------------------------: |
| 准确模式exactly | 队列的副本量count | 集群中队列的副本数(包括主节点),count如果为1表示只有单个副本:即队列主节点。count为2表示2个副本:1主队列和1个镜像队列。换句话说:count = 镜像数量 + 1。如果集群中的节点数量小于count,则该队列将镜像到所有节点。如果有集群总数大于count + 1,且如果包含镜像的节点出现故障,则将在另一个节点上创建一个新的镜像。 |
| all | (none) | 队列在集群中的所有节点之间镜像。队列将镜像到任何新加入的节点。镜像到所有节点会对所有集群节点施加额外的压力,如网络IO,磁盘IO,推荐使用exactly模式,设置副本数量为(n/2 + 1) |
| nodes | node names | 指定队列创建到哪些具体节点,如果指定的系欸但不存在,则出现异常。 |
仲裁队列的出现主要是为了解决镜像队列数据丢失的问题,RabbitMQ3.8以后才有的新功能,用来代替镜像队列,具备以下特征:
仲裁队列的添加十分简单:
[外链图片转存中…(img-O30YYAFk-1675922744941)]
测试使用SpringAMQP代码声明仲裁队列:
首先如果是集群状态,要先通过SpringAMQP连接集群,需要在yaml中配置:
spring:
rabbitmq:
addresses: 192.168.150.xxx:8071, 192.168.150.xxx:8072, 192.168.150.xxx:8073
username: guest
password: guest
virtual-host: /
使用@Bean创建仲裁队列:
@Bean
public Queue quorumQueue(){
return QueueBuilder.durable("quorum.queue2")
.quorum()//设置为仲裁队列
.build();
}