RabbitMQ常见幂等性、可靠性、顺序性问题及解决方案

如何保证幂等性

如果消息的重复消费对业务有影响,那么就需要对消息进行幂等处理,下面介绍消息幂等的概念、场景和处理方法。

什么是幂等性

在数学和计算机科学中,幂等运算可以多次应用而不改变初始应用后的结果。在消息队列服务中,幂等性用于处理相同消息的重复消费。消费者重复消费一条消息,最终消费结果与初次消费结果相同,重复消费不会对业务系统造成负面影响。

例如:消费者根据扣款信息扣减订单货款,付款金额为100元,但由于网络问题,消息重复发送给消费者。结果就是消息被重复消费,但是只扣了一次货款,订单只有一次100元的扣款记录。该例子在消息消费过程中实现了消息幂等性,扣费满足业务需求。

重复消费产生的场景

在互联网应用中,尤其网络较差的情况下,RabbitMQ消息可能会被重复消费,如果消息的重复消费对业务有影响,可以对消息进行幂等处理,以下场景可能会重复消费消息:

  • 生产者重复向RabbitMQ代理的消息队列发送消息
    消息发送到代理并持久化后,由于网络断开或者客户端崩溃,代理未能回复客户端,导致生产者认为代理没有收到消息而重新发送,结果消费者收到两条具有相同内容和消息ID的消息
  • RabbitMQ代理向消费者重复传递消息
    消息发送给消费者后,由于网络断开等原因,消费者客户端没有向broker返回ACK响应,代理不知道消息是否被消费,为了确保消息至少被消费一次,代理在网络恢复后再次传递消息,结果消费者就收到了两条具有相同内容和消息ID的消息。

解决方案

  • 消费数据只是单纯的写入数据库
    可以在生产消息的时候为每一个消息加一个全局唯一ID,消费数据插入数据库之前根据主键ID判断数据是否存在,或者建立联合主键索引,重复插入时会报错
  • 消费数据只是写入redis中
    不需要处理,因为redis天然具有幂等性
  • 复杂业务情况
    将所有消费过的消息ID存入redis,使用redis进行消费判断,和数据库判断相比更快

如何保证可靠性

产生原因

可靠性是指消息在MQ中传输会发生消息丢失问题,若是涉及金钱相关的业务可能会造成巨大损失,一般发生消息丢失会存在以下三种情况

  • 生产者弄丢了消息
    生产者在将数据发送到MQ时,由于网络原因造成投递失败
  • MQ自身弄丢了消息
    未开启RabbitMQ持久化,数据只存储在内存,当MQ宕机造成数据丢失
  • 消费者弄丢了消息
    消费者接收到消息后还没处理完成就宕机了
    在这里插入图片描述

解决方案

  • 生产者弄丢了消息
    方法一:生产者发送数据之前开启事务(不推荐,基于同步消息通讯模式,太慢)
    方法二:生产者开启confirm模式
    1.在application.yml开启confirm模式
    spring:
      rabbitmq:
        addresses: 127.0.0.1
        port: 5672
        username: guest
        password: guest
        # 发送者开启 confirm 确认机制
        publisher-confirm-type: correlated
    
    2.实现ConfirmCallback回调接口,可自定义消息发送失败的处理逻辑
    @Slf4j
    public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
    
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            if (!ack) {
                log.error("消息发送异常!");
                //可以进行重发等操作
            } else {
                log.info("发送者已经收到确认,correlationData={} ,ack={}, cause={}", correlationData, ack, cause);
            }
        }
    }
    
    3.为RabbitTemplate配置回调函数
    @Slf4j
    @Configuration
    public class RabbitMqConfig {
    
         @Bean
        public ConfirmCallbackService confirmCallbackService() {
            return new ConfirmCallbackService();
        }
        
        @Bean
        public RabbitTemplate rabbitTemplate(@Autowired CachingConnectionFactory factory) {
            RabbitTemplate rabbitTemplate = new RabbitTemplate(factory);
    
            /**
             * 消费者确认收到消息后,手动ack回执回调处理
             */
            rabbitTemplate.setConfirmCallback(confirmCallbackService());
            return rabbitTemplate;
        }
        
        //其他配置代码
        ......
    }
    
  • MQ自身弄丢了消息
    开启broker持久化功能
    1.创建queue时设置为持久化队列
    @Bean(QUEUE_IOT_TOIN)
    public Queue createIotQueue() {
        return new Queue(QUEUE_IOT_TOIN, true);
    }
    
    2.发送消息时将消息的deliveryMode设置为持久化
    public void sendToUploadMsg(Object obj, String routingKey) {
        try {
    
            String jsonString = JSON.toJSONString(obj);
            rabbitTemplate.convertAndSend(EXCHANGE_IOT, routingKey, jsonString, message -> {
                //设置该条消息持久化
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return message;
            }, new CorrelationData(UUIDUtil.generate()));
        } catch (Exception e) {
            log.info(routingKey + "发送消息异常!");
        }
    }
    
  • 消费者弄丢了消息
    关闭自动ack,使用手动ack,RabbitMQ中有一个ACK机制,默认情况下消费者接收到到消息,RabbitMQ会自动提交ACK,之后这条消息就不会再发送给消费者了。我们可以更改为手动ACK模式,每次处理完消息之后,再手动ack一下。
    1.修改application.yml配置文件更改为手动ack模式
    spring:
      rabbitmq:
        addresses: 127.0.0.1
        port: 5672
        username: guest
        password: guest
        # 发送者开启 confirm 确认机制
        publisher-confirm-type: correlated
        # 发送者开启 return 确认机制
        publisher-returns: true
        listener:
          simple:
            concurrency: 10
            max-concurrency: 10
            prefetch: 1
            auto-startup: true
            default-requeue-rejected: true
            # 设置消费端手动 ack
            acknowledge-mode: manual
            # 是否支持重试
            retry:
              enabled: true
    
    2.消费端手动ack参考代码:
    @RabbitHandler
    public void handlerMq(String msg, Channel channel, Message message) throws IOException {
        try {
            //业务处理代码
            ......
        
            //手动ACK
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            
        } catch (Exception e) {
            if (message.getMessageProperties().getRedelivered()) {
                log.error("消息已重复处理失败,拒绝再次接收...", e);
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            } else {
                log.error("消息即将再次返回队列处理...", e);
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
        }
    }
    

在这里插入图片描述

如何保证顺序性

产生原因

在生产中经常会有一些类似报表系统这样的系统,需要做 MySQL 的 binlog 同步。比如订单系统要同步订单表的数据到大数据部门的 MySQL 库中用于报表统计分析,通常的做法是基于 Canal 这样的中间件去监听订单数据库的 binlog,然后把这些 binlog 发送到 MQ 中,再由消费者从 MQ 中获取 binlog 落地到大数据部门的 MySQL 中。

在这个过程中,可能会有对某个订单的增删改操作,比如有三条 binlog 执行顺序是增加、修改、删除;消费者愣是换了顺序给执行成删除、修改、增加,这样能行吗?肯定是不行的。

对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息,如消费者 A 执行了增加,消费者 B 执行了修改,消费者 C 执行了删除,但是消费者 C 执行比消费者 B 快,消费者 B 又比消费者 A 快,就会导致消费 binlog 执行到数据库的时候顺序错乱,本该顺序是增加、修改、删除,变成了删除、修改、增加。

如下图是 RabbitMQ 可能出现顺序错乱的问题示意图:
在这里插入图片描述

解决方案

RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,生产者发送消息的时候,同一个订单号的消息发送到同一个 queue 中,由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。

如下图是 RabbitMQ 保证消息顺序性的方案:
在这里插入图片描述

参考

https://blog.csdn.net/zw791029369/article/details/109561457
https://xie.infoq.cn/article/c84491a814f99c7b9965732b1

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值