五事务型MQ的最终一致性事务方案--3如何保证事务成功及正确开发

五 事务型MQ的最终一致性事务方案--3 如何保证事务成功及正确开发

3.2.4 RocketMQ的事务消息机制,如何保证事务成功

1 理解RocketMQ如何保证整个事务流程一致性

image-20230612180427396

在rocketMQ实现最终一致性事务时,系统中会出现如下几个问题,影响事务一致性:

1 producer发送失败;
2 producer发送消息,返回消息是否成功;
3 执行本地事务失败的处理;
4 本地事务执行,如果异常,此时如何解决

首先,对于producer.sendMessageInTransaction如果发送失败,那么调用端直接抛出异常,此时后续不会执行;

而对于producer发送half事务消息,根据第三章的源码跟踪可知,producer.send没有返回send_ok,那么不会执行executeLocal方法,那么后续会执行check回查时,一直查不到信息,最终会回滚消息,删除rollback;

对于执行本地事务失败的情况,这里是RocketMQ设计中的一个bug问题,其会catch掉异常,而不再抛出。所以如果执行事务MethodA>producer.send>executeLocal中执行事务消息入库,这个操作时,其不会是一个整体的事务------就是在executeLocal中抛出异常,不会被@Transactional的方法A所catch,因此出现事务不一致的情况。(该处bug在下面的小节中会展示源码)

2 rocketMQ的client在事务消息实现中的bug

事务消息的发送过程,在前面已经分析过源码,步骤如下:

STEP1 为消息添加prepared、producer-group属性。设置producerGroup目的是,在查询local事务状态时,可以从group中随机选择一个producer即可;
STEP2 调用同步发送,发送消息到rocketMQ;

STEP3 根据sendResult的发送状态,确定本地事务的状态
	CASE1 OK当发送结果成功时,执行本地事务executeLocalTransaction方法,来设置事务执行状态;
	CASE2 如果消息刷新超时,slave不可用,标记事务状态为rollback;
	CASE3 其他情况,事务状态仍旧是unknown
STEP4 结束事务 this.endTransaction();
STEP5 构造事务消息发送的方法返回result;
2.1 producer.sendMessageInTransaction如果不对sendResult为ok判断

根据上述流程可知,本地事务的执行,需要再发送结果时send_ok的情况下,才会执行本地事务executeLocalTransaction。因此,常规情况下,需要对sendResult判断,如果返回不是send_ok,那么就抛异常。(因为,如果没有返回ok,那么由于网络的不确定性,可能是half消息没有发送成功、或者是broker的成功响应因为网络问题没有发送到producer端)。那么最好还是加上sendResult的返回结果判定。

如果不加结果判定,那么对于half后置一类,因为其executeLocalTransaction没有实际方法,影响不大(但是仍旧可能会出现half没有发送到broker端的隐患)

2.2 executeLocalTransaction抛异常问题

可以看到,在step3--》case1 发送结果返回ok的情况下,会执行本地方法executeLocalTransaction,如果本地事务异常,会被catch掉,但是没有重新throw,因此不会被调用方感知。

所以,如果最终一致性事务的实现方案是类似@Transactional add( serviceA--->producer.sendTransactionMessage-->saveTransaction本地事务)这一流程,如果在本地事务执行过程中,saveTransaction出现异常,当前操作不会成功,但是由于一场exception会被rocketMQ捕获,且不再继续抛出,因此异常不会被事务方法add感知,导致serviceA执行成功,但是本地事务不成功。然后执行回查时,会回滚消息。然后后续的serviceB不会消费到消息。

此时,造成事务不一致的结果:serviceA执行成功,但是serviceB无法消费到消息,事务无法执行。

在事务消息的发送过程的详细源码如下:

DefaultMQProducerImpl
  
    public TransactionSendResult sendMessageInTransaction(final Message msg,
        final LocalTransactionExecuter localTransactionExecuter, final Object arg)
        throws MQClientException {
  //this.defaultMQProducer,如果是TransactionMQProducer,则从中中获取监听器
        TransactionListener transactionListener = getCheckListener();
        if (null == localTransactionExecuter && null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }

        // 忽略配置的 DelayTimeLevel 延迟时间级别的参数
        if (msg.getDelayTimeLevel() != 0) {
            MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
        }

        Validators.checkMessage(msg, this.defaultMQProducer);

        SendResult sendResult = null;
  /**…………………………………………………………………………………………为消息添加属性……………………………………………………………………………………………………*/
  //STEP1 为消息添加prepared、producer-group属性。设置producerGroup目的是,在查询local事务状态时,可以从group中随机选择一个producer即可
  		//设置参数prepared为true
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
  		//设置消息所在的生产者组
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {
          //TAG2 this.send(msg)
   //STEP2 同步调用,发送消息到rocketMQ
            sendResult = this.send(msg);
        } catch (Exception e) {
            throw new MQClientException("send message Exception", e);
        }
/** …………………………………………………………………………根据sendResult的发送状态,确定本地事务的状态………………………………………………………………*/
        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
        Throwable localException = null;
        switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                  //为消息添加事务id的属性
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                  //传入为null,不执行
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                      //step3 执行本地事务executeLocalTransaction方法
                        localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    }
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                    if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                        log.info("executeLocalTransactionBranch return {}", localTransactionState);
                        log.info(msg.toString());
                    }
                } 
/**………………………………………………………………step3 执行本地事务executeLocalTransaction方法如果抛异常,这里会catch,并不再throw……………………………… */
              catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    localException = e; //在executeLocalTransaction执行中,如果抛出异常,会被catch掉,但是没有重新throw,因此不会被调用方感知
                }
            }
            break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            case SLAVE_NOT_AVAILABLE:
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                break;
            default:
                break;
        }

  //step4 结束事务
        try {
            this.endTransaction(sendResult, localTransactionState, localException);
        } catch (Exception e) {
            log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
        }

  //step5 构造事务消息发送的方法返回result
        TransactionSendResult transactionSendResult = new TransactionSendResult();
        transactionSendResult.setSendStatus(sendResult.getSendStatus());
        transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
        transactionSendResult.setMsgId(sendResult.getMsgId());
        transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
        transactionSendResult.setTransactionId(sendResult.getTransactionId());
        transactionSendResult.setLocalTransactionState(localTransactionState);
        return transactionSendResult;
    }

3 RocketMQ事务最终一致性如何正确开发

在使用RocketMQ事务消息特性,实现分布式事务最终一致性时,是serviceA---》rocketMQ---》serviceB,同时根据rocketmq事务消息中两阶段执行的特点--先发送half事务消息,然后如果发送成功,执行本地事务,然后发送rollback/commit(如果server一定时间没有从client端获取到事务执行状态,那么会执行回查),回查需要查询本地的事务log情况,根据是否入库,判断serviceA是否执行成功。

根据如上的开发过程,会出现如下三种具体的实现方案:

image-20230612180457034

下面看下三种不同实现方法,会出现什么问题:

3.1 发送half后置

image-20230609151431703

image-20230609151444987

当前half后置发送的开发,订单服务+事务消息落库+producer.sendHalf都是在一个事务中,并且注意去判断发送消息返回是否为send_ok。

分析当前开发模式,是否保证事务一致性:

1 订单数据或者事务消息有异常,由于同在一个事务中,因此事务回滚;
2 如果half消息发送异常,此时异常catch,log记录异常,同时throw抛出,外层事务方法可以感知,因此事务方法回滚;
3 对sendResult的发送结果判断事务是否发送成功,如果发送结果不是send_ok,那么需要抛出异常,此时执行事务回滚。(注意,如果send发送成功,就正常返回,当前事务执行成功)
------如果忽略对sendResult.ok的判断,那么就是发送完half消息就不管是否发送成功,那么half可能存在网络故障等问题不能成功发送

根据上述分析,事务流程能够保证各个方法调用出现异常的最终一致性。

3.2 发送half中置(本地方法中抛异常阻隔事务一致性)

image-20230609152321113

image-20230612180523652

half发送中置的情况,下单和producer.send是在同一个事务方法中,事务消息持久化在executeLocalTransaction方法中。

根据当前开发模式,分析其事务一致性在各个环节出现异常时,事务一致性是否能够保证:

1 如果下单异常,那么事务方法回滚,此时half的send不会执行,没问题;
2 如果half发送失败(发送失败、发送结果不为ok),事务回滚,此时没问题;
3 如果发送send_ok,那么此时下单操作完成,但是executeLocalTransaction执行失败:
	case1 如果catch异常不抛出,返回rollback---此时事务消息落库失败,half消息rollback,但是由于没有throw,那么订单的事务方法感知不到异常。会出现订单落库、事务消息存储失败的情况;
	case2 如果catch异常并throw,此时由于rocketmq代码的bug,此时异常会被catch,但是没有重新抛出,所以,订单的事务方法中感知不到事务异常,此时会出现同样的问题:下订单成功、事务日志持久化失败

因此,根据上述的分析,由于rocketMQ的bug,当前开发方案会有事务不一致的情况。不使用该方案。

3.3 发送half前置

image-20230609154217913

image-20230609154309436

image-20230609154321355

当前模式,把业务方法和事务消息持久化的操作,统一放在executeLocalTransaction方法中。

分析当前方案,在各环节出现异常,一致性能否保证:

1 如果producer.sendHalf异常或者不是send_ok,那么抛异常。此时本地事务不执行;
2 如果本地事务executeLocalTransaction中抛异常:
由于调用事务方法,将订单落库+事务信息持久化放在一个@Transactional方法中,那么如果抛异常,当前事务回滚。不会落库。同时异常会抛向executeLocalTransaction方法,其被rocketMQ捕获,此时不会返回commit_message。那么broker不会得到事务状态,回去启动本地回查。

此时,当前方案开发没有什么问题。

总结:

上述三种方案,half消息中置方案,会因为executeLocalTransaction的异常bug问题,不能保证事务一致性,因此不予考虑。

但是其他两个方案,都可以使用。

但是,对于half消息前置,就是把所有的事务执行放在executeLocalTransaction中执行,会略微有些问题:

1 如果业务方法耗时,那么执行executeLocalTransaction的线程执行时间过长,可能会增加不必要的回查;而half消息后置,把业务方法先执行,那么会减少不必要的事务回查;

2 half消息前置,需要把更多的业务消息包装,通过sendMessageInTransaction(message,object)中object传输,那么会增加类型转换和model对象的构造、解析的代码。

4 consumer端如何保证事务一致性

对于分布式事务情况下,由于系统的复杂和流程繁复,如果consumer端消费失败而去执行回滚的话,需要付出更多的代价,而且还会引发其他系统回退导致的新问题。因此,面对这种概率低的事件,对于consumer端会返回reconsume_later,并重发消息,且默认重试16次,直到消费成功。如果失败,用人工介入处理。

考虑两种解决方案:

1 设置消息重试次数,如果达到指定次数,就发邮件或者短信通知人工介入;

2 等待消息重试次数超过默认的16次,进入死信队列,然后程序监听对应的私信队列主题,通知人工介入或者在rocketMQ控制台查看处理。

4.1 设置重试次数、人工介入

设置超时次数,例如3次,如果三次,需要发送邮件通知人工介入。然后再消息监听器messageListenerConcurrently中consumeMessage方法中,对消费异常情况catch捕获,并判断重试次数。

image-20230609125634210

4.2 监听私信队列

消息队列的默认最大重试次数是16次,如果超过这个次数,消息会添加入死信队列---%DLQ%+consumerGroup消费者组名。例如:

image-20230609130431893

然后点击TOPIC配置,修改其perm属性(2 write权限;4 读权限;6 读写权限)。

最后可以在程序中监听当前死信队列主题,如果有消息,通知人工介入。通过幂等式消费、对死信队列的监听,就保证rocketMQ的消息一定会被下游服务消费到。

5 RocketMQ事务消息的限制

  1. 事务消息不支持延时消息和批量消息。
  2. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话 (N = transactionCheckMax) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
  3. 事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionTimeout 参数。(该参数和代码部分,详细见3.2.3 回查事务状态部分的源码分析内容)
  4. 事务性消息可能不止一次被检查或消费。
  5. 提交给用户的目标主题消息可能会失败,它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ 服务器能通过它们的生产者 ID 查询到消费者。
posted @ 2023-06-12 18:37  LeasonXue  阅读(278)  评论(0编辑  收藏  举报