RabbitMQ如何保证消息的幂等性、可靠性、顺序性

如何保证消息的幂等性

所谓的幂等性其实就是保证同一条消息不会重复或者重复消费了也不会对系统数据造成异常。

出现消息重复消费的情况

拿RabbitMQ来说的话,消费者在消费完成一条消息之后会向MQ回复一个ACK(可以配置自动ACK或者手动ACK) 来告诉MQ这条消息已经消费了。假如当消费者消费完数据后,准备回执ACK时,系统挂掉了,MQ是不知道该条消息已经被消费了。所以重启之后MQ会再次发送该条消息,导致消息被重复消费,如果此时没有做幂等性处理,可能就会导致数据错误等问题。

如何保证消息队列消费的幂等性

这一块应该还是要结合业务来选择合适的方法,有以下几个方案:

  • 消费数据为了单纯的写入数据库,可以先根据主键查询数据是否已经存在,如果已经存在了就没必要插入了。或者直接插入也没问题,因为可以利用主键的唯一性来保证数据不会重复插入,重复插入只会报错,但不会出现脏数据。
  • 消费数据只是为了缓存到redis当中,这种情况就是直接往redis中set value了,天然的幂等性。
  • 针对复杂的业务情况,可以在生产消息的时候给每个消息加一个全局唯一ID,消费者消费消息时根据这个ID去redis当中查询之前是否消费过。如果没有消费过,就进行消费并将这个消息的ID写入到redis当中。如果已经消费过了,就无需再次消费了。

如何保证消息的可靠性

在将消息投入到MQ时,有可能会发生消息投递失败、消息丢失的问题,如果刚好丢失的是一些核心消息,例如money相关的,那就凉凉了…

出现消息丢失的情况

还是拿RabbitMQ来说…
image

从图中可以看到一共有以下三种可能出现消息丢失的情况:

  • 生产者弄丢了消息

生产者在将数据发送到MQ的时候,可能由于网络等原因造成消息投递失败

  • MQ自身弄丢了消息

未开启RabbitMQ的持久化,数据存储于内存,服务挂掉后队列数据丢失;
开启了RabbitMQ持久化,消息写入后会持久化到磁盘,但是在落盘的时候挂掉了,不过这种概率很小

  • 消费者弄丢了消息

消费者刚接收到消息还没处理完成,结果消费者挂掉了…

保证消息可靠性的方法

针对以上三种情况,每种情况都有对应的处理方法:

  • 生产者弄丢消息时的解决方法

方法一:生产者在发送数据之前开启RabbitMQ的事务

// 开启事务
channel.txSelect
try {
    // 这里发送消息
} catch (Exception e) {
    channel.txRollback
    // 这里再次重发这条消息
}
// 提交事务
channel.txCommit

采用该种方法由于事务机制,会导致吞吐量下降,太消耗性能。

方法二:开启confirm模式

使用springboot时在application.yml配置文件中做如下配置

spring:
  rabbitmq:
    addresses: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    # 发送者开启 confirm 确认机制
    publisher-confirm-type: correlated

实现confirm回调接口

@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);
        }
    }
}

生产者发送消息时设置confirm回调

@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;
    }
    
    //其他配置代码
    ......

}

小结: 事务机制和 confirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm机制是异步的,你发送个消息之后就可以发送下一个消息,RabbitMQ 接收了之后会异步回调confirm接口通知你这个消息接收到了。一般在生产者这块避免数据丢失,建议使用用 confirm 机制。

  • MQ自身弄丢消息时的解决方法

第一步: 创建queue时设置为持久化队列,这样可以保证RabbitMQ持久化queue的元数据,此时还是不会持久化queue里的数据

    @Bean(QUEUE_IOT_TOIN)
    public Queue createIotQueue() {
        return new Queue(QUEUE_IOT_TOIN, true);
    }

第二步: 发送消息时将消息的deliveryMode设置为持久化,此时queue中的消息才会持久化到磁盘。

    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 + "发送消息异常!");
        }
    }

同时设置queue和message持久化以后,RabbitMQ 挂了再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据,保证数据不会丢失。

但是就算开启持久化机制,也有可能出现上面说的的消息落盘时服务挂掉的情况。这时可以考虑结合生产者的confirm机制来处理,持久化机制开启后消息只有成功落盘时才会通过confirm回调通知生产者,所以可以考虑生产者在生产消息时维护一个正在等待消息发送确认的队列,如果超过一定时间还没从confirm中收到对应消息的反馈,自动进行重发处理。

  • 消费者自身弄丢消息时的解决方法

关闭自动ACK,使用手动ACK。RabbitMQ中有一个ACK机制,默认情况下消费者接收到到消息,RabbitMQ会自动提交ACK,之后这条消息就不会再发送给消费者了。我们可以更改为手动ACK模式,每次处理完消息之后,再手动ack一下。不过这样可能会出现刚处理完还没手动ack确认,消费者挂了,导致消息重复消费,不过我们只需要保证幂等性就好了,重复消费也不会造成问题。

在springboot中修改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

消费端手动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);
            }
        }


    }

RabbitMQ保证消息可靠性总结:
image


如何保证消息的顺序性

消息在投入到queue的时候是有顺序,如果只是单个消费者来处理对应的单个queue,是不会出现消息错乱的问题。但是在消费的时候有可能多个消费者消费同一个queue,由于各个消费者处理消息的时间不同,导致消息未能按照预期的顺序处理。其实根本的问题就是如何保证消息按照预期的顺序处理完成

出现消费顺序错乱的情况

  • 为了提高处理效率,一个queue存在多个consumer
    image

  • 一个queue只存在一个consumer,但是为了提高处理效率,consumer中使用了多线程进行处理
    image

保证消息顺序性的方法

  • 将原来的一个queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据) 发送到同一个queue当中。
    image

  • 一个queue就一个consumer,在consumer中维护多个内存队列根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。
    image

RabbitMQ保证消息顺序性总结:
核心思路就是根据业务数据关键值划分成多个消息集合,而且每个消息集合中的消息数据都是有序的,每个消息集合有自己独立的一个consumer。多个消息集合的存在保证了消息消费的效率,每个有序的消息集合对应单个的consumer也保证了消息消费时的有序性。


参考文章:

https://gitee.com/shishan100/Java-Interview-Advanced/blob/master/docs/high-concurrency/how-to-ensure-the-order-of-messages.md

https://www.cnblogs.com/-wenli/p/13047059.html

  • 41
    点赞
  • 227
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值