文章目录
一、发布确认(高级)
1、介绍
在 “RabbitMQ 快速入门(精讲)” 中我们学习了队列与生产者之间的消息确认。但是在实际的生产环境中,会因为一些特殊的情况,导致rabbitMQ服务重启(或宕机),在rabbitmq重启期间,生产者的消息发布失败,导致消息丢失,需要手动处理和恢复。我们思考,何如才能进行rabbitmq消息的可靠发布呢?特别是在极端的情况下,rabbitmq集群不可用时,能够有效的处理发布失败的消息。
对于broker而言,其中有两种结构可能会导致消息的丢失,一种是exchange没有接收到生产者发布的消息,另一种是queue没有接收到exchange传递的消息。具体的解决方法如下:
2、交换机没有接收到生产者的消息时的发布确认
为例更清晰的理解高级确认发布,我们先来探究一下交换机没有接收到生产者的消息时,rabbitmq的解决方法。在rabbitmq中有一个回调结构(函数式接口),
第一步:开启高级发布确认模式
spring:
rabbitmq:
publisher-confirm-type: correlated
# none:禁用发布确认模式(默认)
# correlated:发布消息成功后交换机将会触发回调函数
# simple:(同步发布确认消息)经测试有两种结果,
# 一种是发布消息成功后交换机将会触发回调函数
# 另一种是发布消息成功后使用rabbitTemplate调用waitForConfirm 或 waitForConfirmOrDie
# 方法等待broker节点返回发送结果,根据返回结果判断下一步逻辑,值得注意的是waitForConfirmOrDie
# 方法如果返回的是false,则会关闭信道(chanel),接下来将无法发送消息到broker.
第二步:实现RabbitTemplate.ConfirmCallback接口
@PostConstruct:后构造,是java提供的注解,被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。执行顺序 Constructor(构造方法)
-> @Autowired(依赖注入)
-> @PostConstruct(注释的方法)
rabbitTemplate.setConfirmCallback(this::confirm):因为ConfirmCallback是rabbitTemplate中被用到的,所以我们需要将我们写的实现类中的重写方法出入到rabbitTemplate中。
CorrelationData correlationData:我们可以发现,这个类是作为重写方法confirm
的参数存在的。值得注意的是,correlationData本身是没有id的(correlationData.getId())的,而是在发布消息的时候传递进去的,这也就意味着,我们需要在生产者中编写并传递correlationData。
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author HuXuehao (StudiousTiger)
* @desc 这个类的作用是
* @date 2021/11/2
*/
@Slf4j
@Component
public class CallBackImplConfig implements RabbitTemplate.ConfirmCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
// 将这个实现类中的重写方法注入到rabbitTemplate中
rabbitTemplate.setConfirmCallback(this::confirm);
}
/**
*
* @param correlationData 相关的数据
* @param ack true for ack, false for nack
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
log.info("【success】:交换机【"
+rabbitmqConfig.EXCHANGE+"】成功接收消费者发布tag=【"
+correlationData.getId()+"】的消息");
} else {
log.info("【error】:交换机【{}】没有接收消费者发布tag=【{}】的消息,错误的原因是【{}】"+
rabbitmqConfig.EXCHANGE,
correlationData.getId(),
cause);
}
}
第二步:编写生产者
convertAndSend():此方法有多个重载方法,其中 convertAndSend(String exchange, String routingKey, final Object object,@Nullable CorrelationData correlationData)方法就可以传递CorrelationData 对象。
import com.tiger.config.rabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@Slf4j
@RestController
@RequestMapping("/mq")
public class RabbitmqController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send/{message}/{tag}")
public String send(@PathVariable String message,@PathVariable String tag) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(tag);
rabbitTemplate.convertAndSend(rabbitmqConfig.EXCHANGE,rabbitmqConfig.ROUTING_KEY,message,correlationData);
log.info("【当前时间】:{},发送消息{}给 exchange 交换机",new Date().toString(),message);
return "消息【"+message+"】,成功发送到【exchange】交换机";
}
}
第三步:编写配置类
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 {
// 声明交换机名
public static final String EXCHANGE = "exchange";
// 声明队列名
public static final String QUEUE = "queue";
// binding
public static final String ROUTING_KEY = "rountingkey";
/**
* 声明 交换机 exchange
* @return
*/
@Bean("exchange")
public DirectExchange exchange(){
return ExchangeBuilder.directExchange(EXCHANGE).durable(false).build();
}
/**
* 声明队列 queue
* @return
*/
@Bean("queue")
public Queue queue_c(){
return QueueBuilder.nonDurable(QUEUE).build();
}
/**
* 延迟队列(queue_c)绑定(xc)正常交换机(exchange_X)
* @param queue
* @param exchange
* @return
*/
@Bean
public Binding queueBindingExchange(@Qualifier("queue") Queue queue,
@Qualifier("exchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}
}
第四步:编写消费者
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class DeadLetterQueueCustomer {
// 监听器
@RabbitListener(queues = "queue_c")
public void receiveD(Message message, Channel channel){
byte[] body = message.getBody();
String msg = new String(body);
log.info("【当前时间】:{},接收的消息为 {}",new Date().toString(),msg);
}
}
成功接收消息测试
访问http://localhost:8080/mq/send/hello-world/01
失败接收消息测试
我们将生产者中发送消息时的交换机的名字改错,制造人为的错误
访问http://localhost:8080/mq/send/hello-world/01
3、队列没有接收到生产者的消息时的发布确认
这种模式的实现上与上一种模式的区别在于配置文件
、实现类上
和生产者
上,其他的预上一种模式一样。
配置文件
publisher-returns: true:表示开启发布回退服务
spring:
rabbitmq:
publisher-returns: true
实现类
ReturnedMessage returned:我们可以从中得到消息链路上的一些信息,如:交换机,消息体、原因…
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Component
public class CallBackImplConfig implements RabbitTemplate.ReturnsCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(this::returnedMessage);
}
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("【error】:消息【{}】发,交换机【{}】退回,原因是【{}】",
new String(returned.getMessage().getBody()),
returned.getExchange(),
returned.getReplyText());
}
}
生产者
@GetMapping("/send2/{message}/{tag}")
public String send02(@PathVariable String message,@PathVariable String tag) {
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE, RabbitmqConfig.ROUTING_KEY,message);
log.info("【当前时间】:{},发送消息{}给 exchange 交换机",new Date().toString(),message);
return "消息【"+message+"】,成功发送到【exchange】交换机";
}
访问http://localhost:8080/mq/send2/studious_tiger/02
我们可以人为的将routing key修改,实现队列无法成功接收生产者发布的消息
访问http://localhost:8080/mq/send2/studious_tiger/02
4、总结
上面我们是分开实现的交换机和队列的发布确认,我们可以将二者进行整合,接下来是整合的具体实现
配置文件
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 192.168.174.136
port: 5672
publisher-confirm-type: correlated
publisher-returns: true
配置类
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 {
// 声明交换机名
public static final String EXCHANGE = "exchange";
// 声明队列名
public static final String QUEUE = "queue";
// binding
public static final String ROUTING_KEY = "rountingkey";
/**
* 声明 交换机 exchange
* @return
*/
@Bean("exchange")
public DirectExchange exchange(){
return ExchangeBuilder.directExchange(EXCHANGE).durable(false).build();
}
/**
* 声明队列 queue
* @return
*/
@Bean("queue")
public Queue queue_c(){
return QueueBuilder.nonDurable(QUEUE).build();
}
/**
* 延迟队列(queue_c)绑定(xc)正常交换机(exchange_X)
* @param queue
* @param exchange
* @return
*/
@Bean
public Binding queueBindingExchange(@Qualifier("queue") Queue queue,
@Qualifier("exchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}
}
实现类
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Component
public class CallBackImplConfig implements RabbitTemplate.ConfirmCallback,
RabbitTemplate.ReturnsCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this::confirm);
rabbitTemplate.setReturnsCallback(this::returnedMessage);
}
/**
*
* @param correlationData 相关的数据
* @param ack true for ack, false for nack
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("【success】:交换机【"
+ RabbitmqConfig.EXCHANGE + "】成功接收消费者发布tag=【"
+ correlationData.getId() + "】的消息");
} else {
log.info("【error】:交换机没有接收消费者发布tag=【{}】的消息,错误的原因是【{}】",
correlationData.getId(),
cause);
}
}
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("【error】:消息【{}】发,交换机【{}】退回,原因是【{}】",
new String(returned.getMessage().getBody()),
returned.getExchange(),
returned.getReplyText());
}
}
生产者
import com.tiger.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@Slf4j
@RestController
@RequestMapping("/mq")
public class RabbitmqController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send/{message}/{tag}")
public String send(@PathVariable String message,@PathVariable String tag) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(tag);
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE, RabbitmqConfig.ROUTING_KEY, message,correlationData);
log.info("【当前时间】:{},发送消息{}给 exchange 交换机",new Date().toString(),message);
return "消息【"+message+"】,成功发送到【exchange】交换机";
}
}
消费者
import com.rabbitmq.client.Channel;
import com.tiger.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class DeadLetterQueueCustomer {
// 监听器
@RabbitListener(queues = RabbitmqConfig.QUEUE)
public void receiveD(Message message, Channel channel){
byte[] body = message.getBody();
String msg = new String(body);
log.info("【当前时间】:{},接收的消息为 {}",new Date().toString(),msg);
}
}
二、备份交换机
对于队列无法接收交换机发送的消息的处理方法,我能还可以使用备份交换机的方式进行处理,这样消息接收的信息就不需要重新发给生产者了。而且备份交换机
与RabbitTemplate.ConfirmCallback
实现类是可以并存的,且备份交换机
的优先级是高于RabbitTemplate.ConfirmCallback
实现类的。模型如下:
上图中的模型图与上一个模型图的区别在于:第一,多了一个备份交换机
和一个警告队列
以及一个消费者
。第二,正常交换机
和备份交换机
之间多了一个备份关系(alternate)。
下面我们基于【一、4】中的代码实现。
第一步:声明备份交换机和警告队列,以及他们之间的绑定
// 声明备份交换机名
public static final String ALTERNATE_EXCHANGE = "alternate_exchange";
// 声明警告队列名
public static final String WARNING_QUEUE = "warning_queue";
/**
* 声明 备份交换机 alternate_exchange
* @return
*/
@Bean("alternate_exchange")
public FanoutExchange alternateExchange(){
return ExchangeBuilder.fanoutExchange(ALTERNATE_EXCHANGE).durable(false).build();
}
/**
* 声明警告队列 warning_queue
* @return
*/
@Bean("warning_queue")
public Queue warningQueue(){
return QueueBuilder.nonDurable(WARNING_QUEUE).build();
}
/**
* 绑定
* @param queue 警告队列
* @param exchange 备份交换机
* @return
*/
@Bean
public Binding alternateQueueBindingWarningExchange(@Qualifier("warning_queue") Queue queue,
@Qualifier("alternate_exchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
第二步:声明第二个消费者
// 监听器
@RabbitListener(queues = RabbitmqConfig.WARNING_QUEUE)
public void receive2(Message message, Channel channel){
byte[] body = message.getBody();
String msg = new String(body);
log.info("【备份消费者】:{},接收的消息为 {}",new Date().toString(),msg);
}
第三步:绑定备份关系
我们需要修改一下正常交换机,因为其需要实现与备份交换机之间的备份关系。
alternate(ALTERNATE_EXCHANGE):实现正常交换机与备份交换机之间的绑定关系
/**
* 声明 交换机 exchange
* @return
*/
@Bean("exchange")
public DirectExchange exchange(){
// alternate(ALTERNATE_EXCHANGE) 实现正常交换机与备份交换机之间的连接
return ExchangeBuilder.directExchange(EXCHANGE).durable(false).alternate(ALTERNATE_EXCHANGE).build();
}
访问http://localhost:8080/mq/send2/studious_tiger/01
我们下面测试修改 routing key 之后的效果,实现queue接收不到消息的情况
访问http://localhost:8080/mq/send2/studious_tiger/01
三、MQ 的幂等性问题的产生与解决
1、什么是幂等性
MQ 的消息的 幂等性 问题 : 1、生产者已把消息发送到 mq ,在 mq 给生产者返回ack的时候网络中断,故生产者未收到确定信息,生产者认为消息未发送成功,但实际情况是, mq 已成功接收到了消息,在网络重连后,生产者会重新发送刚才的消息,造成 mq 接收了重复的消息 2、消费者在消费 mq 中的消息时, mq 已把消息发送给消费者,消费者在给 mq 返回ack时网络中断,故 mq 未收到确认信息,该条消息会重新发。
其中最典型的事故就是消费者在提交订单付费的时候因为MQ的幂等性的问题导致二次付费,造成不必要的麻烦。
上述的两种情况就是MQ的消息的幂等性。
2、解决思路
①.唯一 ID + 指纹码 机制,利用数据库主键去重
为了应对用户在一瞬间的频繁操作,这个指纹码可能是我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中。
好处就是实现简单,就一个拼接,然后查询判断是否重复。
坏处就是在高并发时,如果是单个数据库就会有写入性能瓶颈
解决方案 :根据 ID 进行分库分表,对 id 进行算法路由,落到一个具体的数据库,然后当这个 id 第二次来又会落到这个数据库,这时候就像我单库时的查重一样了。利用算法路由把单库的幂等变成多库的幂等,分摊数据流量压力,提高性能。
②.利用redis的原子性去实现
使用 redis 的原子性(setnx)去实现需要考虑两个点
一是 ,是否 要进行数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性? 数据库与缓存进行同步肯定要进行写操作,到底先写 redis 还是先写数据库,这是个问题,涉及到缓存更新与淘汰的问题
二是,如果不落库,那么都存储到缓存中,如何设置定时同步的策略? 不入库的话,可以使用双重缓存等策略,保障一个消息副本,具体同步可以使用类似 databus 这种同步工具。
四、集群
我们现在来搭建集群,终于到了搭建集群的时候了。我的思路是使用docker实现集群的搭建,原因如下,我是在个人机上进行操作的,计算机的资源有限。第二,docker给我们提供了 docker network ,可以实现与不同linux上相同的相关(唯一一点遗憾的是,每个容器的端口绑定是不同的)。
1、搭建docker局域网
在搭建局域网的时候,我被坑了,大家搭建的时候一定要注意。docker的局域网是万万不能包含linux宿主机上的网段的,这点尤为的重要。
创建docker的局域网
docker network create --driver bridge --subnet 10.0.0.0/24 --gateway 10.0.0.1 myNet
执行完成之后,我们可以使用 docker network ls
进行查看
2、启动是三个rabbitmq容器
我们将实现一个 一主两从
的集群
--hostname rabbitX:设置容器的主机名,在搭建集群的时候,我们会用到
-e RABBITMQ_ERLANG_COOKIE=‘rabbitcookie’:Erlang Cookie值必须相同,也就是RABBITMQ_ERLANG_COOKIE参数的值必须相同。因为RabbitMQ是用Erlang实现的,Erlang Cookie相当于不同节点之间相互通讯的秘钥,Erlang节点通过交换Erlang Cookie获得认证。
启动第一个容器
docker run -di --hostname rabbit1 --name myrabbit01 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --net myNet rabbitmq:management
启动第二个容器
docker run -di --hostname rabbit2 --name myrabbit02 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15673:15672 -p 5673:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --net myNet rabbitmq:management
启动第三个容器
docker run -di --hostname rabbit3 --name myrabbit03 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15674:15672 -p 5674:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --net myNet rabbitmq:management
3、配置rabbitmq连接,实现集群
搭建集群之前,每一个rabbitmq需要被停止并重置
对于 myrabbit01 容器
docker exec -it myrabbit01 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit
对于 myrabbit02 容器
rabbitmqctl join_cluster [-–ram] {cluster_node}:表示将节点加入指定集群中,参数“–ram”表示同步 rabbit@rabbit1的内存节点,忽略次参数默认为磁盘节点。
docker exec -it myrabbit02 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit
对于 myrabbit03 容器
rabbitmqctl join_cluster [-–ram] {cluster_node}:表示将节点加入指定集群中,参数“–ram”表示同步 rabbit@rabbit1的内存节点,忽略次参数默认为磁盘节点。
docker exec -it myrabbit03 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit
访问http://192.168.174.136:15672,账户为admin,密码为admin。集群搭建成功
集群节点脱离命令
一下的命令是让 myrabbit02
节点脱离 myrabbit01
节点
docker exec -it myrabbit01 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl forget_cluster_node rabbit@rabbit2
让myrabbit01节点忘记myrabbit02节点
exit
4、镜像队列
我们上面虽然已经实现一rabbitmq的集群,但是在集群上的一节点A创建的队列并不能备份到其他的节点上,这也就意味着,当A节点宕机之后,会导致队列的丢失。
我们需要给集群配置策略,实现镜像队列——在节点A上的队列会被备份当其他的节点,即使A节点发生了宕机等意外情况,其他节点上的备份队列依然可以使用。
添加“镜像队列策略”
第一步:添加策略
策略可以在任何一个节点上尽心添加
第二步:创建一个镜像队列
创建一个前缀为mirror的队列
mirror_queue1的特点是存在镜像队列
发现mirror_queue1的镜像队列在rabbit3节点上
第三步:停掉rabbit1,看看效果
执行docker stop myrabbit01
后,发现rabbit1停掉了
我们看一看队列的情况
发现因为我们的设置策略,当原本的节点宕机之后,会原本的镜像节点会变成直接节点,然后会将队列进行重新备份。
经过测试,即使rabbit1重新启动,【第三步】中的队列与镜像队列不会因为rabbit1重新启动发生变化。
4、实现负载均衡
我们可以发现一个问题,即使我们实现了集群,但是,当一个节点宕机之后,代码依然不会自动去访问另一个节点,这是因为在代码中我们已经向rabbitmq的请求IP写死了。
解决上述问题的方法有两种,一种是使用try-catch进行异常捕获,当发现异常,我们可以去访问其他的节点。在这里建议镜像队列的策略中的ha-params
的值填写和你的rabbitmq的集群的个数一致
,因为镜像队列的生成事随机的,在每一个节点上进行备份,方便我们访问。
第二种方法是我们可以通过 Nginx 或 LVS 或 HAProxy 实现负载均衡,在这里就不过多的介绍的,后续有机会再给大家更新吧…
Nginx、LVS、HAProxy负载均衡软件的优缺点详解:http://www.ha97.com/5646.html
✈ ❀ 希望平凡の我,可以给你不凡の体验 ☂ ✿ …