唠个嗑
网络上搜罗了多次想知道 RabbitMQ 现实业务种怎么实现消息的可靠性的,但是大多都不太理想,站在各位大佬巨人的肩膀上研究了一段时间,我也整理了一套简单可行性的方案,包括消息异常处理。这篇文章想主要讲一些业务处理方案,项目中加入 RabbitMQ 中间件很简单,但是根据具体业务实现消息的可靠性,这个需要多加考虑。当然下面也会通过测试代码来分析,文末也会附上源码地址。
1、准备
1.1、环境准备
之前博客上写过一篇编译安装的方法 地址,大家可以参考,因为 RabbitMQ 底层语言的原因可能稍微麻烦点,那就没有办法了吗?如果你是先搞测试,再在项目中使用的话,那可以使用 docker 安装,2 行代码,如下
docker pull rabbitmq
docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management
1.2、理论准备
RabbitMQ 的类型包括:direct、topic、fanout、headers、system(翻源码看到的)
这里主要通过 topic 来分析,bindingkey 可以通过通配符 # 和 * 来匹配多个 路由键 (routingKey),
bindingkey 是绑定交换机(exchange)和队列(queue)的, 生产者(publisher)发消息的时候会携带 routingKey、exchange 和 消息发送给 RabbitMQ,
连接成功后实际是组件 exchange 接收了生产者的消息,然后通过 bindingkey 匹配 routingKey,决定送给哪个 queue,每个消费者都会有 queue,所以 queue接收到消息 后就可以确保消费者接可以收到消息了,最后消费者再消费。
再详细的内容可以查看大佬 erlie 的总结 地址
2、消息确认
RabbitMQ 基础配置
pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.yml
spring:
rabbitmq:
host: 192.168.1.105
port: 5672
username: guest
password: guest
virtual-host: /
# 交换机、队列和绑定键声明
test:
exchange: test.exchange
one:
queue: one.test.queue
binding-key: one.test.key
consumer
@RabbitListener(bindings=@QueueBinding(
//配置交换器
exchange=@Exchange(value="${test.exchange}",type= ExchangeTypes.TOPIC),
//配置路由键
key="${test.one.binding-key}",
//配置队列名称
value=@Queue(value="${test.one.queue}",autoDelete="true")
))
public void test(String msg) {
log.info("test 收到的消息为:[{}]", msg);
//业务代码...
}
publisher
@Autowired
private RabbitTemplate rabbitTemplate;
@Value("${test.exchange}")
private String exchange;
@Value("${test.one.binding-key}")
private String routingKey;
public void test() {
rabbitTemplate.convertAndSend(exchange, routingKey, "test msg");
}
通过上面默认的配置基本上就可以使用 RabbitMQ 了,但是这不是本篇的重点.我们要知道消息发送和到消费的过程中出现问题怎么办?这就需要我们分段确认消息是否接收成功,如果失败了该如何处理,先想想可以怎么做。
我们先捋一下思路,消息发送给 RabbitMQ ,如果连接 RabbitMQ 失败,则记录该消息,如果连接成功但是 exchange 接收失败则记录下该消息,如果 exchange 接收成功但是 queue 接收 exchange 的消息失败则记录下该消息,消息从生产者到 queue 有 3 个位置可能因网络抖动或其他原因出现问题,那我们在这三个位置记录下问题后,统一通过计划定时获取记录的消息并且重新发送,如果重发三次还没有成功则标记该记录为异常消息。
2.1、生产者消息确认和回调
这里分两步:
- 消息确认指的是 RabbitMQ(exchange) 确认接收到了消费者发送的消息
- 消息回调指的是 queue 接收 exchange 的消息失败,则回调告诉 RabbitMQ 失败的消息
2.1.1、消息确认
开启配置
spring:
rabbitmq:
#publisher-confirms: true #已过时
publisher-confirm-type: correlated #开启生产者消息确认
还有另外 2 种模式:
- none 值是禁用发布确认模式,是默认值;
- simple 值经测试有两种效果,其一效果和 correlated 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回false则会关闭 channel,则接下来无法发送消息到 broker。
当然还需新建一个类实现 RabbitTemplate.ConfirmCallback,重写方法 confirm,该方法有三个参数 correlationData、ack、cause,主要说下 ack,值为 true 表示 exchange 成功接收到消息,false 表示 exchange 接收消息失败,这里 2 种结果可以分别处理,比如 false 可以把接收失败的消息入库,然后通过定时器来处理,比较懂的同学现在可能就有疑问了,correlationData 只能得到 msgId,根本没有具体的消息,这里可以发挥你出色的想象力,可以通过对象封装得到,也可以通过存内存或者磁盘存储得到,方法总比困难多。
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String msgId = correlationData.getId();
if (ack) {
log.info("成功发送给 mq, msgId:[{}]", msgId);
} else {
log.error("发送给 mq 失败, msgId:[{}], 失败理由cause:[{}]", msgId, cause);
//消息从生产者没有到 exchange,那存库
saveToDB(msgId,...);
}
}
这里还差一步,就是原生的 rabbitTemplate 怎么知道消息确认时使用刚建的类呢,注入即可
rabbitTemplate.setConfirmCallback(刚建的类);如有疑问可看文末源码。
2.1.2、消息回调
spring:
rabbitmq:
publisher-returns: true #开启生产者消息回调
同上,需要新建类并实现 RabbitTemplate.ReturnCallback,并且重写方法 returnedMessage, 最后需要注入如下内容
rabbitTemplate.setReturnCallback(刚建的类);
// 要想使 returnCallback 生效,必须设置为true
rabbitTemplate.setMandatory(true);
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
String msgId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");
String msg = new String(message.getBody());
log.error("消息回调 msgId:[{}], msg:[{}] 不能被正确路由,replyCode:[{}], replyText:[{}], exchange:[{}], routingKey:[{}]", msgId, msg, replyCode, replyText, exchange, routingKey);
//消息从 exchange 没有到 queue, 那存库
saveToDB(msgId, exchange, routingKey, msg);
}
如果消息发送时走到了回调方法 returnedMessage 中,说明目前的消息有问题是需要处理的,同上,入库。定时器来处理。
当然消息的发送方法 rabbitTemplate.convertAndSend() 会多一个参数 correlationData
具体处理方法可以参考源码,这里只提供思路。
2.2、消费者消息确认
消费者消息确认是确认消费了队列中的消息,如果出现问题 RabbitMQ 会有重试机制,长时间失败则需要人工干预,这个和生产者的确认是先后关系,实际是没有关联关系的,说到这,说下我之前转的牛角尖,一直想寻找 exchange 如何确认消费者成功消费消息,但是无果,后来细想,RabbitMQ 应该设计的就是消费者和 queue 交互,没必要和 exchange 交互。如有大佬知道 exchange 如何 ack 消费者消费消息可以告诉小弟,不胜感激。
圆规正转,上消费者代码
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #开启消费者消息确认; none:自动确认、auto:根据情况确认
@RabbitListener(bindings=@QueueBinding(
//配置交换器
exchange=@Exchange(value="${test.exchange}",type= ExchangeTypes.TOPIC),
//配置路由键
key="${test.one.binding-key}",
//配置队列名称
value=@Queue(value="${test.one.queue}",autoDelete="true")
))
public void test(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("test 收到的消息为:[{}], msgId:[{}]", msg, message.getMessageProperties().getHeaders().get("spring_returned_message_correlation"));
try {
//业务处理
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
主要说 3 个方法:
1、basicAck 是确认消息,需要传递两个参数
- deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
- multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息,false则只确认传入值等于 delivery_tag 的消息
2、basicNack 是拒绝消息,可以拒绝多条,比 basicAck 多一个布尔值的参数,如果为 true,被 nack 后重新入队列然后重新消费消费;如果为 false 被 nack 就丢了。
3、basicReject 只能拒绝一条消息,reject 后消息直接丢了。
总结
这里简单实现 RabbitMQ 消息可靠的方式是通过把消息发送时出现问题后直接入库,然后通过计划定时查询再重新发送给 RabbitMQ,如果 exchange 成功 ack 后则标记为重发成功,如果重发 3 次还是失败则标记异常,需要人工处理。
讨论
这种处理其实不算是最优的方案,技术上还可以有如下方案
- 生产者发消息时记录该条消息,并设该记录 1 分钟后生效,留 1 分钟给exchange 确认并直接标记该消息记录为成功,然后计划任务定时扫有效且未确认的消息,再发送给 RabbitMQ ,如果确认后则标记为成功,否则 3 次后标记为失败。
- 还和小编写的方案类似,记录失败的消息,但是定时任务获取到失败的消息后,直接调用消费者的服务处理,不通过 RabbitMQ, 但是这就需要维护消息和消费者服务的关系了,稍微复杂些。
最后希望可以帮到看官,如果记录的不对烦请评论指出,一起讨论
https://github.com/charmsongo/springboot-samples/tree/master/springboot-rabbitmq-songo