不止于纸上谈兵,用代码案例分析如何确保RabbitMQ消息可靠性?

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

文章导图

在这里插入图片描述

可靠性分析-RabbitMQ 消息丢失的三种情况

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

在这里插入图片描述

从图中可以看出 RabbitMQ 发送消息时可能发生的三种丢失情况:

  1. 消息在传输过程中丢失
    生产者发送消息时,消息在网络传输或其他原因导致消息没有成功到达 RabbitMQ 队列。
  2. RabbitMQ 收到消息后丢失
    RabbitMQ 收到了消息,但由于 RabbitMQ 内部问题(如宕机、内存泄漏等),消息没有持久化,导致消息丢失。
  3. 消费者接收消息后丢失
    消费者成功接收到消息,但处理过程中出现异常或未能成功确认(acknowledge),RabbitMQ 认为消息已被处理,但实际上消息未被成功处理。

接下来我们就一一分析这三种情况

生产者发送可靠性消息实现2种方式

1、采用事务消息

如果是采用rocketMQ可以直接采用rocketMQ本身实现的事务消息,不需要额外自己实现了!关于事务消息的文章在我的另外一篇文章有专门介绍:

传送门: 消息队列与分布式事务:探讨不同MQ如何实现可靠消息最终一致性

事务消息是投递消息的一种方式,可以确保业务执行成功,消息一定会投递成功。

事务消息投递方案设计

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);
        }
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值