消息中间件如何保证消息不丢失

一、消息队列MQ的三个阶段

1、生产者发送消息到MQ

2、MQ存储消息到内存或者硬盘

3、消费者消费消息

由于网络的原因、服务器的原因、程序的原因等等,在每个阶段都有可能引起消息的丢失:

1、生产者发送消息到MQ:这个阶段可能由于网络延迟导致mq消息丢失

2、MQ存储消息到内存或者硬盘:Broker将消息先放到内存,然后根据刷盘策略持久化到硬盘上,但是刚收到消息,还没持久化到硬盘服务器宕机了,消息就会丢失

3、消费者消费消息:MQ由于网络原因在传输过程中把消息传丢了,同时MQ也从队列中把消息删除了,或者消费者消费失败消息丢失了

接下来详细描述一下三大主流队列:rocketmq、rabbitmq、kafka是如何保证消息不丢失的

二、RocketMQ

1、第一阶段:消息发送到MQ

选择不同的消息模式发送消息

RocketMQ发送消息有三种模式,即同步发送,异步发送、单向发送。

  • 同步发送消息:会同步阻塞等待Broker返回发送结果,发送失败不会收到发送结果SendResult,这种是最可靠的发送方式。
  • 异步发送消息:可以在回调方法中得知发送结果。
  • 单向发送消息:发送完之后就不管了,不管发送成功没成功,是最不可靠的一种方式。

同步和异步都是可靠的消息发送模式,应用可以根据消息的重要性和不同场景,选择不同的模式。

   /**
     * @description: 单向发送 这种方式主要用在不特别关心发送结果的场景,例如日志发送。
     */
    public void sendMq() {
        for (int i = 0; i < 10; i++) {
            rocketMQTemplate.convertAndSend("RocketMQ-test", "测试-单向消息发送-" + i);
        }
    }
 
/***********************************************************************************/
  /**
     * @description: 同步发送 可靠性同步地发送 使用的比较广泛,比如:重要的消息通知,短信通知。

     */
    public void sync() {
        SendResult sendResult = rocketMQTemplate.syncSend("RocketMQ-test", "sync同步发送消息。");
        log.info("发送结果:{}", sendResult);
    }
 
/***********************************************************************************/
 /**
     * @description: 异步发送
     * 异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
     */
    public void async() {
        String msg = "async异步发送消息。";
        log.info(">msg:<<" + msg);
        rocketMQTemplate.asyncSend("RocketMQ-test", msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult var1) {
                log.info("异步发送成功{}", var1);
            }
 
            @Override
            public void onException(Throwable var1) {
                //发送失败可以执行重试
                log.info("异步发送失败{}", var1);
            }
        });
    }

消息生产者发送消息失败的重试机制

RocketMQ为生产者提供了失败重试机制,同步发送和异步发送默认都是失败重试两次,当然可以修改重试次数,如果多次还是失败,那么可以采取记录这条信息,然后人工采取补偿机制。 

 可以通过配置来灵活的设置消息发送失败重试次数

 2、第二阶段:MQ存储消息到内存或者硬盘

刷盘策略

RocketMq持久化消息有两种策略,即同步刷盘和异步刷盘。通过修改配置文件来完成:ASYNC_FLUSH=异步刷盘(默认方式),SYNC_FLUSH=同步刷盘。

异步刷盘:此模式下当生产者把消息发送到broker,消息存到内存之后就认为消息发送成功了,就会返回给生产者消息发送成功的结果。但是如果消息还没持久化到硬盘,服务器宕机了,那么消息就会丢失。

同步刷盘:当Broker接收到消息并且持久化到硬盘之后才会返回消息发送成功的结果,这样就会保证消息不会丢失,但是同步刷盘相对于异步刷盘来说效率上有所降低,大概降低10%,具体情况根据业务需求设定吧。

3、第三阶段:消费者消费消息

手动ack

/**
 * @description: 消费端确认消息消费成功的消费者
 */
@Component
@Slf4j
public class consumerAck implements MessageListenerConcurrently {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        for (MessageExt msg:msgs){
           log.info("接收到的消息是-----{}",new String(msg.getBody()));
        }
        //MQ收到SUCCESS即认为消费成功
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

消费者消费失败重试机制

消费者消费失败会自动重试,如果消费失败并且没有手动ack则会自动重试15次。

三、RabbitMQ

1、第一阶段:消息发送到MQ

事务机制:类似RocketMQ的同步消息发送模式,会降低系统的性能,一般不采用

    public static void main(String[] args) {
        try {
            System.out.println("生产者启动成功..");
            // 1.创建连接
            connection = MyConnection.getConnection();
            // 2.创建通道
            channel = connection.createChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String msg = "测试事务机制保证消息发送可靠性。。。。";
            channel.txSelect(); //开启事务
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            //发生异常时,mq中并没有新的消息入队列
            //int i=1/0;
            //没有发生异常,提交事务
            channel.txCommit();
            System.out.println("生产者发送消息成功:" + msg);
        } catch (Exception e) {
            e.printStackTrace();
            //发生异常则回滚事务
            try {
                if (channel != null) {
                    channel.txRollback();
                }
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        } finally {
            try {
                if (channel != null) {
                    channel.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
 
        }
    }

确认机制:当mq收到生产者发送的消息时,会返回一个ack告知生产者,收到了这条消息,如果没有收到,那就采取重试机制后者其他方式补偿。

 #开启生产者确认模式
 publisher-confirm-type: correlated
 # 打开消息返回,如果投递失败,会返回消息
 publisher-returns: true  
 
 #publisher-confirm-type有3种取值
 #NONE值是禁用发布确认模式,是默认值
 #CORRELATED值是发布消息成功到交换器后会触发回调方法
 #SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法 
@Component
@Slf4j
public class ConfirmCallBackListener implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    @PostConstruct
    public void init() {
        //指定 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }
 
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info("correlation--{},ack--{},cause--{}", correlationData, ack, cause);
        if (ack) {
            //确认收到消息
        } else {
            //收到消息失败,可以自定义重试机制,或者将失败的存起来,进行补偿
        }
    }
 
    /*
     *
     * @param returnedMessage
     * 消息是否从Exchange路由到Queue, 只有消息从Exchange路由到Queue失败才会回调这个方法
     * @return void
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info("被退回信息是》》》》》》{}", returnedMessage.getMessage());
        log.info("replyCode》》》》》》{}", returnedMessage.getReplyCode());
        log.info("replyText》》》》》》{}", returnedMessage.getReplyText());
        log.info("exchange》》》》》》{}", returnedMessage.getExchange());
        log.info("routingKey>>>>>>>{}", returnedMessage.getRoutingKey());
    }
}

重试机制:默认3次,可以修改重试次数,超过了最大重试次数限制采取人工补偿机制。

2、第二阶段:MQ存储消息到内存或者硬盘

开启消息持久化机制

RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。要保证rabbitMQ不丢失消息,那么就需要开启rabbitMQ的持久化机制,即把消息持久化到硬盘上,这样即使rabbitMQ挂掉在重启后仍然可以从硬盘读取消息。要想做到消息持久化,必须满足以下三个条件,缺一不可。

  • Exchange 设置持久化
  • Queue 设置持久化
  • Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息

消息补偿机制

  • 生产端首先将业务数据以及消息数据入库,需要在同一个事务中,消息数据入库失败,则整体回滚。
  • 根据消息表中消息状态,失败则进行消息补偿措施,重新发送消息处理。

死信队列:如果队列满了,多余的消息发送到Broker时可以使用死信队列保证消息不会被丢弃 

3、第三阶段:消费者消费消息

开启消费端的手动ack

@Component
@Slf4j
public class SnailConsumer {
 
    @RabbitListener(queues = "snail_direct_queue")
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
        // 获取消息Id
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        log.info("获取到的消息>>>>>>>{},消息id>>>>>>{}", msg, messageId);
        try {
            int result = 1 / 0;
            System.out.println("result" + result);
            // // 手动ack
            Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            // 手动签收
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            //拒绝消费消息(丢失消息) 给死信队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

 四、kafka

1、第一阶段:消息发送到MQ

producer的ack机制

kafka的生产者确认机制有三种取值即三个模式:

acks = 0 :生产者将不会等待来自服务器的任何确认,该记录将立即添加到套接字缓冲区并视为已发送。在这种情况下,无法保证服务器已收到记录,并且重试配置将不会生效(因为客户端通常不会知道任何故障)。
acks = 1 :leader会将记录写入其本地日志,但无需等待所有follwer服务器的完全确认即可做出回应,在这种情况下,当leader还没有将数据同步到Follwer宕机,存在丢失数据的可能性。
acks = -1:代表所有的所有的分区副本备份完成,不会丢失数据这是最强有力的保证。但是这种模式往往效率相对较低。

producer重试机制 

2、第二阶段:MQ存储消息到内存或者硬盘

kafka的broker使用副本机制保证数据的可靠性。每个broker中的partition我们一般都会设置有replication(副本)的个数,生产者写入的时候首先根据分发策略(有partition按partition,有key按key,都没有轮询)写入到leader中,follower(副本)再跟leader同步数据,这样有了备份,也可以保证消息数据的不丢失。

3、第三阶段:消费者消费消息

手动ack

    /*
     *
     * @param message
     * @param ack
     * @手动提交ack
     * containerFactory  手动提交消息ack
     * errorHandler 消费端异常处理器
     * @return void
     */
    @KafkaListener(containerFactory = "manualListenerContainerFactory", topics = "test-topic", errorHandler = "consumerAwareListenerErrorHandler")
    public void onMessageManual(List<ConsumerRecord<?, ?>> record, Acknowledgment ack) {
        for (int i=0;i<record.size();i++){
            System.out.println(record.get(i).value());
        }
        ack.acknowledge();//直接提交offset
    }

offset commit

消费者通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,会接着上次的offset进行消费。kafka并不像其他消息队列,消费完消息之后,会将数据从队列中删除,而是维护了一个日志文件,通过时间和储存大小进行日志删除策略。如果offset没有提交,程序启动之后,会从上次消费的位置继续消费,有可能存在重复消费的情况。

Offset Reset 三种模式

earliest(最早):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费。
latest(最新的):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据。
none(没有):topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常。

猜你喜欢

转载自blog.csdn.net/m0_37258559/article/details/130246811
今日推荐