RocketMQ如何保证分布式事务最终一致性?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daijiguo/article/details/89059963

在这里插入图片描述

步骤:

step1:
  实线1向MQ发送消息,此时消息对consumer不可见,即不可被消费;
  虚线1执行本地事务并提交事务。

step2:
  执行本地事务的回调函数executeLocalTransaction()可以有三种返回值:

LocalTransactionState.COMMIT_MESSAGE;
LocalTransactionState.ROLLBACK_MESSAGE;
LocalTransactionState.UNKNOW

  将本地事务执行状态返回给broker,
  如果是COMMIT_MESSAGE,则将不可见消息变为可消费;
  如果是ROLLBACK_MESSAGE,broker会回收step1中的不可见消息,同时producer本地请手动回退(自己设计的回退方案,而非数据库的rollback,因为在虚线1中你已经将pruducer本地事务进行了commit);
  如果是UNKNOW或者producer与broker网络异常,broker会定时主动回调producer中的回调函数checkLocalTransaction()去判断producer本地事务是否正常(一般是查询数据库中数据是否已经持久化),broker定时主动查看producer本地事务的逻辑应该是写在rocket server的代码中,目前尚未找到源码,但是查阅各种资料和博客,都有提到。

step3:
  消费者消费消息。

step4:
  如果有消息消费失败了,则将失败的消息回传给broker,即重新写入commitLog文件,消费者将重新消费;如果消息回传的时候,consumer和broker之间网络断开,则consumer会调用submitConsumeRequestLater()方法,在consumer端进行重新消费,如果仍然消费失败,会不断重试直到达到默认的16次,你可以使用msg.getReconsumeTimes()方法来获取当前重试次数,如果重试次数足够多之后仍然无法消费成功,必须通过工单、日志等方式进行人工干预以让producer事务进行回退处理。


疑问:

Question1:如何保持发送prepare消息和producer本地事务的事务一致性?

  这个问题可以拆解成两个问题:
  1、producer本地事务执行成功,但是prepare消息还没发送,怎么办?
  prepare消息发送和producer本地事务执行是顺序执行的,只有prepare消息发送成功了,才会继续执行producer本地事务,因此,该种情况不存在,见代码:

public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter tranExecuter, final Object arg){
    SendResult sendResult = null;
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());

    // 1. 第一阶段,发送 PREPARED 消息
    try {
        sendResult = this.send(msg);
    } catch (Exception e) {
        throw new MQClientException("send message Exception", e);
    }

    LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;

    switch (sendResult.getSendStatus()) {
        case SEND_OK: {
            try {
                // 第二阶段,执行本地事务操作
                localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
                if (null == localTransactionState) {
                    localTransactionState = LocalTransactionState.UNKNOW;
                }
            } catch (Throwable e) {
                ....
            }
        }
        break;
        case FLUSH_DISK_TIMEOUT:
        case FLUSH_SLAVE_TIMEOUT:
        case SLAVE_NOT_AVAILABLE:
            localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
            break;
        default:
            break;
    }

    // 到这一步,如果前面的都成功了,LocalTransactionState 应该是COMMIT_MESSAGE, 否则应该是 ROLLBACK_MESSAGE 或 UNKNOWN

    // 第三阶段,会根据 LocalTransactionState 的值,发送不同类型的请求给 broker 去确认第一阶段发的消息。
    try {
        this.endTransaction(sendResult, localTransactionState, localException);
    } catch (Exception e) {
        .....
    }
    .....
}

  2、prepare消息发送成功,但是producer本地事务执行失败,怎么办?
  如step2中描述的,不管是producer本地事务执行失败,返回LocalTransactionState.UNKNOW,还是producer与broker网络异常,broker都会定时主动回调producer中的回调函数checkLocalTransaction()去判断producer本地事务是否正常(一般是查询数据库中数据是否已经持久化),返回LocalTransactionState.COMMIT_MESSAGE或者LocalTransactionState.ROLLBACK_MESSAGE同时producer本地手动回退(自己设计的回退方案,而非数据库的rollback)。

Question2:执行step3和step4时,网络断开怎么办,会影响分布式事务一致性吗?

  先介绍两个概念:
  强一致性:要求无论更新操作是在哪个数据副本上执行,之后所有的读操作都要能获得最新的数据。
  最终一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们将这段时间称为"不一致性窗口",我们仅能保证用户最终能够读取到某操作对系统特定数据的更新。
  因此,不管是step2、step3还是step4过程中网络断开,其实producer本地事务都已提交完成,都无法保证强一致性,因此TCC方案、MQ方案其实都是在保证事务的最终一致性,而非强一致性。
  step3网络断开没关系,因为在pushconsumer+clustering的消费方式向,broker端保存了offset,无论consumer何时下线,一旦consumer上线,都会从offset处开始消费,我们不必为其操心。
  step4网络断开怎么办?我们首先要明白step4存在的意义,step4之所以存在,是为了将消费失败的消息重新发回broker,让consumer进行重新消费,如果step4网络断开,是不是就意味着consumer无法重新消费这个消息了呢?非也,consumer端如果感知到网络断开,会立即将该消息存入失败队列,并重新消费失败队列中的消息,代码如下:

   public void processConsumeResult(
        final ConsumeConcurrentlyStatus status,
        final ConsumeConcurrentlyContext context,
        final ConsumeRequest consumeRequest
    ) {
        int ackIndex = context.getAckIndex();

        if (consumeRequest.getMsgs().isEmpty())
            return;

        switch (status) {
            case CONSUME_SUCCESS:
                if (ackIndex >= consumeRequest.getMsgs().size()) {
                    ackIndex = consumeRequest.getMsgs().size() - 1;
                }
                int ok = ackIndex + 1;
                int failed = consumeRequest.getMsgs().size() - ok;
                this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
                break;
            case RECONSUME_LATER:
                ackIndex = -1;
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
                    consumeRequest.getMsgs().size());
                break;
            default:
                break;
        }

        switch (this.defaultMQPushConsumer.getMessageModel()) {
            case BROADCASTING:
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
                }
                break;
            case CLUSTERING:
                List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    //将所有消息都重新发送到broker中的commitLog文件,以让consumer重新消费
                    boolean result = this.sendMessageBack(msg, context);
                    //如果发送回broker的时候网络断开,则将这些消息加入msgBackFailed列表,立即重新消费,详情见submitConsumeRequestLater()函数
                    if (!result) {
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                        msgBackFailed.add(msg);
                    }
                }

                if (!msgBackFailed.isEmpty()) {
                    consumeRequest.getMsgs().removeAll(msgBackFailed);

                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }

        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
        }
    }

  因此,step4虽然断开了,但我们最终目的是重新消费某些消费失败的消息,最终目的并没有受到影响,因此并不会影响我们的最终一致性。如果重新消费仍然失败,会不断重试直到达到默认的16次,你可以使用msg.getReconsumeTimes()方法来获取当前重试次数,如果重试次数足够多之后仍然无法消费成功,必须通过工单、日志等方式进行人工干预以让producer事务进行回退处理。

说明:由于consumer可能会重新消费某些消息很多次,因此,应保证consumer端事务的幂等性,如insert into语句可以使用replace into来代替。
参考:
https://blog.csdn.net/zzzgd_666/article/details/80882107
http://www.iocoder.cn/RocketMQ/message-pull-and-consume-second/#defaultmqpushconsumerimplsendmessageback
https://help.aliyun.com/knowledge_detail/54318.html
https://blog.csdn.net/prestigeding/article/details/78998683
https://fdx321.github.io/2017/08/23/【RocketMQ源码学习】9-事务消息/
https://mp.weixin.qq.com/s/vgohXl1LxYk3CyDI8WHxwA
https://cloud.tencent.com/developer/article/1015442
https://mp.weixin.qq.com/s/9A6ZnpBmAbQYC7kLr1iZCQ

展开阅读全文

没有更多推荐了,返回首页