| RabbitMQ系列文章 |
|---|
| 深入RabbitMQ世界:探索3种队列、4种交换机、7大工作模式及常见概念 |
| 不止于纸上谈兵,用代码案例分析如何确保RabbitMQ消息可靠性? |
| 不止于方案,用代码示例讲解RabbitMQ顺序消费 |
| RabbitMQ常见问题持续汇总 |
文章导图

可靠性分析-RabbitMQ 消息丢失的三种情况
关于如何保证消息可靠性,在网上搜索方案解决能搜出很多,但是关于对应的代码却很少有人去实现分析,所以本篇文章,我不止会讨论方案如何实现,还会有对应的代码讲解,让你更好地理解!

从图中可以看出 RabbitMQ 发送消息时可能发生的三种丢失情况:
- 消息在传输过程中丢失
生产者发送消息时,消息在网络传输或其他原因导致消息没有成功到达 RabbitMQ 队列。 - RabbitMQ 收到消息后丢失
RabbitMQ 收到了消息,但由于 RabbitMQ 内部问题(如宕机、内存泄漏等),消息没有持久化,导致消息丢失。 - 消费者接收消息后丢失
消费者成功接收到消息,但处理过程中出现异常或未能成功确认(acknowledge),RabbitMQ 认为消息已被处理,但实际上消息未被成功处理。
接下来我们就一一分析这三种情况
生产者发送可靠性消息实现2种方式
1、采用事务消息
如果是采用rocketMQ可以直接采用rocketMQ本身实现的事务消息,不需要额外自己实现了!关于事务消息的文章在我的另外一篇文章有专门介绍:
事务消息是投递消息的一种方式,可以确保业务执行成功,消息一定会投递成功。
事务消息投递方案设计
1、本地库创建一个消息表(t_msg)
create table if not exists t_msg
(
id varchar(32) not null primary key comment '消息id',
body_json text not null comment '消息体,json格式',
status smallint not null default 0 comment '消息状态,0:待投递到mq,1:投递成功,2:投递失败',
fail_msg text comment 'status=2 时,记录消息投递失败的原因',
fail_count int not null default 0 comment '已投递失败次数',
send_retry smallint not null default 1 comment '投递MQ失败了,是否还需要重试?1:是,0:否',
next_retry_time datetime comment '投递失败后,下次重试时间',
create_time datetime comment '创建时间',
update_time datetime comment '最近更新时间',
key idx_status (status)
) comment '本地消息表'
2、事务消息投递的过程
- step1:开启本地事务
- step2:执行本地业务
- step3:消息表t_msg写入记录,status为0(待投递到MQ)
- step4:提交本地事务
- step5:若事务提交成功,则投递消息到MQ,然后将t_msg中的status置为1(投递成功);本地事务失败的情况不用考虑,此时消息记录也没有写到db中

知识点拓展:Spring事务同步器
知识点拓展: 如何判断事务是否提交成功呢?这就涉及Spring的事务同步器了
TransactionSynchronizationManager.registerSynchronization 是 Java Spring 框架中的一个方法,它用于注册事务同步处理器(TransactionSynchronization)。事务同步处理器是 Spring 事务管理的一个特性,允许你在事务的边界内执行一些操作,无论是事务提交还是回滚。
具体来说,TransactionSynchronizationManager 负责管理事务同步操作的注册和执行。当你调用 registerSynchronization 方法时,你可以传入一个实现了 TransactionSynchronization 接口的实例。这个实例定义了在事务的不同阶段(如开始、提交、回滚)应该执行哪些操作。
以下是 TransactionSynchronization 接口中定义的一些方法,这些方法可以在事务的不同生命周期点被调用:
beforeCommit(boolean readOnly): 在事务提交之前调用,如果事务是只读的,则readOnly参数为true。beforeCompletion(): 在事务实际提交或回滚之前调用,用于执行清理操作。afterCommit(): 如果事务提交成功,则调用此方法。afterCompletion(int status): 在事务完成后调用,无论事务是提交还是回滚。status参数指示事务的状态:STATUS_COMMITTED表示提交成功,STATUS_ROLLED_BACK表示已回滚。
/**
* 若有事务,则在事务执行完毕之后,进行投递
*
* spring事务扩展点,通过TransactionSynchronizationManager.registerSynchronization添加一个事务同步器TransactionSynchronization,
* 事务执行完成之后,不管事务成功还是失败,都会调用TransactionSynchronization#afterCompletion 方法
*/
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
/**
* 代码走到这里时,事务已经完成了(可能是回滚了、或者是提交了)
* 看下本地消息记录是否存在?如果存在,说明事务是成功的,业务是执行成功的,则投递消息 & 并将消息状态置为成功
*/
//为了提升性能:事务消息的投递消息这里异步去执行,即使失败了,会有补偿JOB进行重试
mqExecutor.execute(() -> deliverMsg(msgPOList));
}
});
3、异常情况
step5失败了,其他步骤都成功,此时业务执行成功,但是消息投递失败了,此时需要有个job来进行补偿,对于投递失败的消息进行重试。
4、消息投递补偿job
这个job负责从本地t_msg表中查询出状态为0记录或者失败需要重试的记录,然后进行重新投递到MQ。
对于投递失败的,采用衰减的方式进行重试,比如第1次失败了,则10秒后,继续重试,若还是失败,则再过20秒,再次重试,需要设置一个最大重试次数,最终还是投递失败,则需要告警+人工干预。
核心代码讲解
发送事务消息
这里按照上面的消息投递流程,在提交完本地事务以后,通过TransactionSynchronizationManager.registerSynchronization添加一个事务同步器TransactionSynchronization,这样事务执行完成之后,不管事务成功还是失败,都会调用TransactionSynchronization#afterCompletion 方法,然后我们在里面处理对应的逻辑即可:
- 看下本地消息记录是否存在?如果存在且状态还是未投递,说明事务是成功的,业务是执行成功的,则投递消息 & 并将消息状态置为成功
- 如果本地消息记录为空,说明本地事务回滚了,那么消息表中的记录也会自动事务回滚,不需要额外处理
@Transactional
public void sendMessage(String bodyJson) {
Message message = new Message();
message.setMessageId(UUID.randomUUID().toString());
message.setBodyJson(bodyJson);
message.setCreateTime(LocalDateTime.now());
message.setUpdateTime(LocalDateTime.now());
// Step 1: 开启本地事务
// Step 2: 执行本地业务
// Step 3: 消息表写入记录,status为0
message.setStatus(0);
messageRepository.save(message);
// Step 4: 提交本地事务
/**
* 若有事务,则在事务执行完毕之后,进行投递
*
* spring事务扩展点,通过TransactionSynchronizationManager.registerSynchronization添加一个事务同步器TransactionSynchronization,
* 事务执行完成之后,不管事务成功还是失败,都会调用TransactionSynchronization#afterCompletion 方法
*/
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
/**
* 代码走到这里时,事务已经完成了(可能是回滚了、或者是提交了)
* 看下本地消息记录是否存在?如果存在,说明事务是成功的,业务是执行成功的,则投递消息 & 并将消息状态置为成功
*/
Message msg=messageRepository.findById(message.getMessageId());
if (msg != null && msg.getStatus() == 0) {
// Step 5: 投递消息到MQ
rabbitTemplate.convertAndSend("exchangeName", "routingKey", bodyJson);
// 更新消息状态
msg.setStatus(1);
msg.setUpdateTime(LocalDateTime.now());
msg.save(message);
}
//如果msg==null,说明本地事务回滚了,那么消息表中的记录也会自动事务回滚,不需要额外处理
}
});
}
定时任务补偿处理失败消息
这里用定时任务扫描出状态为0或者status=2且retry=1,并且他们的重试时间在未来2分钟内(2分钟是为了避免一次性查出所有对数据库造成较大压力)要重试的消息:
- 如果发送成功,将状态设置为1代表发送成功了
- 如果发送异常,则将状态设置为2代表发送失败,同时根据已经重试次数是否小于5次(可以根据自己业务设定)设置是否需要继续重试
@Scheduled(cron = "*/60 * * * * ?") // 每1分钟执行一次
public void retryFailedMessages() {
// 查询状态为0或者status=2且retry=1且重试时间在未来2分钟内要重试的消息,sql是这样的
//select m from message m where (m.status = 0 and m.nextRetryTime<=当前时间 + 2分钟) or (m.status = 2 and m.sendRetry = true and m.nextRetryTime<=当前时间 + 2分钟)
List<Message> messages = messageRepository.findMessagesToSend(0);
for (Message message : messages) {
// 尝试重新发送
try {
rabbitTemplate.convertAndSend("exchangeName", "routingKey", message.getBodyJson());
message.setStatus(1); // 设置为已发送成功
messageRepository.save(message);
}

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



