【RocketMQ】揭开事务消息的神秘面纱

【RocketMQ】揭开事务消息的神秘面纱

参考资料:

解析 RocketMQ 业务消息——“事务消息”—— 阿里云

RocketMQ事务消息, 图文、源码学习探究~

RocketMQ实战一:事务消息保证分布式事务的一致性

RocketMQ源码分析13:事务消息

《RocketMQ技术内幕》

一、为什么需要事务消息

以电商交易场景为例,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更。当前业务的处理分支包括:

  • 主分支订单系统状态更新:由未支付变更为支付成功;
  • 物流系统状态新增:新增待发货物流记录,创建订单物流记录;
  • 积分系统状态变更:变更用户积分,更新用户积分表;
  • 购物车系统状态变更:清空购物车,更新用户购物车记录。

image-20231001171231757

1.1、代码示例

我们先看下代码:

public void createOrder(Order order) {
    try {
        //先发送消息
        SendResult send = producer.send(orderMessage);
        if (send.getSendStatus() == SendStatus.SEND_OK) {
            orderMapper.save(order);
            orderDetailMapper.save(order.getOrderDetail());

            //提交事务
            connection.commit();
        }
    } catch (Exception e) {
        //回滚
        connection.rollback();
    }
}    

这样也是有问题的:

  1. 首先他也存在先写库后发消息的问题,一旦MQ由于网络等原因长时间没有返回SendResult给生产者,将会导致本地事务无法被提交或回滚,高并发下资源将会被快速耗尽。
  2. 其次,生产者将消息发送出去并快速响应了,但是执行本地数据库事务时出现了错误,比如上述代码中的orderMapper.save(order)执行出错了,这也就意味着消息已经发送出去,消费者可以消费了,但是此时本地事务失败了,为了弥补错误,此时可能需要“回滚”之前发送的消息,但是此时这条消息可能已经被消费了,就算没有被消费,每次我都在发送消息后判断是否出现了异常,如果出现了异常在发送条"回滚"的消息,这无疑是增加了开发的复杂度,也显得冗余。

分布式系统调用的特点是:一个核心业务逻辑的执行,同时需要调用多个下游业务进行处理。因此,如何保证核心业务和多个下游业务的执行结果完全一致,是分布式事务需要解决的主要问题。

1.2、传统 XA 事务方案:性能不足

为了保证上述四个分支的执行结果一致性,典型方案是基于XA协议的分布式事务系统来实现。将四个调用分支封装成包含四个独立事务分支的大事务,基于XA分布式事务的方案可以满足业务处理结果的正确性,但最大的缺点是多分支环境下资源锁定范围大,并发度低,随着下游分支的增加,系统性能会越来越差。

1.3、基于普通消息方案:一致性保障困难

将上述基于 XA 事务的方案进行简化,将订单系统变更作为本地事务,剩下的系统变更作为普通消息的下游来执行,事务分支简化成普通消息+订单表事务,充分利用消息异步化的能力缩短链路,提高并发度。

image-20231001171603942

该方案中消息下游分支和订单系统变更的主分支很容易出现不一致的现象,例如:

  • 消息发送成功,订单没有执行成功,需要回滚整个事务;
  • 订单执行成功,消息没有发送成功,需要额外补偿才能发现不一致;
  • 消息发送超时未知,此时无法判断需要回滚订单还是提交订单变更。
1.4、基于RocketMQ分布式事务消息:支持最终一致性

上述普通消息方案中,普通消息和订单事务无法保证一致的本质原因是普通消息无法像单机数据库事务一样,具备提交、回滚和统一协调的能力。

而基于消息队列 RocketMQ 版实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。

image-20231001171710113

二、事务消息的基本原理

2.1、概念介绍
  • 事务消息:RocketMQ 提供类似 XA 或 Open XA 的分布式事务功能,通过 RocketMQ 事务消息能达到分布式事务的最终一致;
  • 半事务消息:暂不能投递的消息,生产者已经成功地将消息发送到了 RocketMQ 服务端,但是 RocketMQ 服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息;
  • 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,RocketMQ 服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。
2.2、事务消息生命周期

image-20231001171914912

  • 初始化:半事务消息被生产者构建并完成初始化,待发送到服务端的状态;
  • 事务待提交:半事务消息被发送到服务端,和普通消息不同,并不会直接被服务端持久化,而是会被单独存储到事务存储系统中,等待第二阶段本地事务返回执行结果后再提交。此时消息对下游消费者不可见;
  • 消息回滚:第二阶段如果事务执行结果明确为回滚,服务端会将半事务消息回滚,该事务消息流程终止;
  • 提交待消费:第二阶段如果事务执行结果明确为提交,服务端会将半事务消息重新存储到普通存储系统中,此时消息对下游消费者可见,等待被消费者获取并消费;
  • 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ 会对消息进行重试处理。具体信息,请参见消息重试;
  • 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败);RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
  • 消息删除:当消息存储时长到期或存储空间不足时,RocketMQ 会按照滚动机制清理最早保存的消息数据,将消息从物理文件中删除。
2.3、事务消息的设计

了解RocketMQ的事务消息的设计之前,我们先了解这些基本概念。

XA(Unix Transaction)是一种分布式事务解决方案,一种分布式事务处理模式,是基于XA协议的。XA协议由Tuxedo(Transaction for Unix has been Extended for Distributed Operation,分布式操作扩展之后的Unix事务系统)首先提出的,并交给X/Open组织,作为资源管理器与事务管理器的接口标准.

XA模式中有三个重要组件:TCTMRM

  • TC :Transaction Coordinator,事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。

RocketMQ中Broker充当着TC。

  • TM : Transaction Manager,事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务。它实际是全局事务的发起者。

RocketMQ中事务消息的Producer充当着TM。

  • RM : Resource Manager,资源管理器。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

RocketMQ中事务消息的Producer及Broker均是RM

XA模式是一个典型的2PC,其执行原理如下:

  1. TM向TC发起指令,开启一个全局事务。

  2. 根据业务要求,各个RM会逐个向TC注册分支事务,然后TC会逐个向RM发出预执行指令。

  3. 各个RM在接收到指令后会在进行本地事务预执行。

  4. RM将预执行结果上报给TC。当然,这个结果可能是成功,也可能是失败。

  5. TC在接收到各个RM的Report后会将汇总结果上报给TM,根据汇总结果TM会向TC发出确认指令。TC在接收到指令后再次向RM发送确认指令。

    1. 若所有结果都是成功响应,则向TC发送Global Commit指令。
    2. 只要有结果是失败响应,则向TC发送Global Rollback指令。
  6. TC在接收到指令后再次向RM发送确认指令。

了解这些基本概念后,我们在回过头来看RocketMQ的事务消息的设计

2.3.1、事务消息在一阶段对用户不可见

在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

在RocketMQ中,消息在服务端的存储结构如下,每条消息都会有对应的索引信息,Consumer通过ConsumeQueue这个二级索引来读取消息实体内容,其流程如下:

image-20231022230717614

RocketMQ的具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中,正因为消息主题被替换,故消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费。其实改变消息主题是RocketMQ的常用“套路”,回想一下延时消息的实现机制。

2.3.2、Commit和Rollback操作以及Op消息的引入

在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。先说Rollback的情况。对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。

2.3.3、Op消息的存储和对应关系

RocketMQ将Op消息写入到全局一个特定的Topic中通过源码中的方法—TransactionalMessageUtil.buildOpTopic();这个Topic是一个内部的Topic(像Half消息的Topic一样),不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作.

image-20231022230804662

2.3.4、Half消息的索引构建

在执行二阶段Commit操作时,需要构建出Half消息的索引。一阶段的Half消息由于是写到一个特殊的Topic,所以二阶段构建索引时需要读取出Half消息,并将Topic和Queue替换成真正的目标的Topic和Queue,之后通过一次普通消息的写入操作来生成一条对用户可见的消息。所以RocketMQ事务消息二阶段其实是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程。

2.3.5、如何处理二阶段失败的消息?

如果在RocketMQ事务消息的二阶段过程中失败了,例如在做Commit操作时,出现网络问题导致Commit失败,那么需要通过一定的策略使这条消息最终被Commit。RocketMQ采用了一种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback。Broker端通过对比Half消息和Op消息进行事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。

值得注意的是,rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。

2.4、实现流程

事务消息交互流程如下图所示:

image-20231001172129565

  1. 生产者将消息发送至 RocketMQ 服务端;
  2. RocketMQ 服务端将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息被标记为“暂不能投递”,这种状态下的消息即为半事务消息;
  3. 生产者开始执行本地事务逻辑;
  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit 或是 Rollback),服务端收到确认结果后处理逻辑如下:
    • 二次确认结果为 Commit:服务端将半事务消息标记为可投递,并投递给消费者;
    • 二次确认结果为 Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
  5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查;
  6. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果;
  7. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行处理。
2.5、官方示例代码

通过代码注释,我们可以比较直观地发现,RocketMq事务发送事务消息与普通消息的首要区别就在于发送的API,当然除此之外,事务消息还会设置TransactionListenerRocketMq两阶段提交就与TransactionListener密不可分~

public class TransactionProducer {

  public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
  public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
  public static final String TOPIC = "TopicTest1234";

  public static final int MESSAGE_COUNT = 10;

  public static void main(String[] args) throws MQClientException, InterruptedException {
    TransactionListener transactionListener = new TransactionListenerImpl();
    TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);

    // 设置事务监听器
    producer.setTransactionListener(transactionListener);
    producer.start();

    String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
    for (int i = 0; i < MESSAGE_COUNT; i++) {
      try {
        Message msg =
          new Message(TOPIC, tags[i % tags.length], "KEY" + i,
                      ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
        
        // 发送事务消息
        SendResult sendResult = producer.sendMessageInTransaction(msg, null);
        System.out.printf("%s%n", sendResult);

        Thread.sleep(10);
      } catch (MQClientException | UnsupportedEncodingException e) {
        e.printStackTrace();
      }
    }

    for (int i = 0; i < 100000; i++) {
      Thread.sleep(1000);
    }
    producer.shutdown();
  }
}

事务消息监听器

public class TransactionListenerImpl implements TransactionListener {
  private AtomicInteger transactionIndex = new AtomicInteger(0);

  private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

  @Override
  public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    // 执行本地事务
    int value = transactionIndex.getAndIncrement();
    int status = value % 3;
    localTrans.put(msg.getTransactionId(), status);
    return LocalTransactionState.UNKNOW;
  }

  @Override
  public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    // 处理事务回查
    Integer status = localTrans.get(msg.getTransactionId());
    if (null != status) {
      switch (status) {
        case 0:
          return LocalTransactionState.UNKNOW;
        case 1:
          return LocalTransactionState.COMMIT_MESSAGE;
        case 2:
          return LocalTransactionState.ROLLBACK_MESSAGE;
        default:
          return LocalTransactionState.COMMIT_MESSAGE;
      }
    }
    return LocalTransactionState.COMMIT_MESSAGE;
  }
}

三、源码分析事物消息

3.1、事务消息发送流程

关键实现类

TransactionMQProducer:

  • TransactionListener transactionListener:事务监听器,主要定义实现本地事务状态执行、本地事务状态回查两个接口。
  • ExecutorService executorService:事务状态回查异步执行线程池。

image-20231001173859537

TransactionListener

  • LocalTransactionState executeLocalTransaction (final Message msg. final Objeet arg):执行本地事务.
  • LocalTransactionState checkLocal Transaction 一 final MessageExt msg):事务消息状态回查.

image-20231001173947253

3.1.1、为消息添加属性,同步发送消息

Step1:首先为消息添加属性,TRAN MSG 和 PGROUP,分别表示消息为 prepare 消息、 消息所属消息生产者组。设置消息生产者组的目的是在查询事务消息本地事务状态时,从该生产者组中随机选择一个消息生产者即可,然后通过同步调用方式向 RocketMQ 发送消息,其发送消息的流程在上文中有详细的分析,在本文稍后会重点关注针对事务消息所做的特殊处理。

【RocketMQ】浅谈消息发送机制

image-20231001174414141

3.1.2、根据结果执行相应操作

Step2:根据消息发送结果执行相应的操作。

  • 如果消息发送成功,则执行 TransactionListener #executeLocalTransaction 方法,该方法的职责是记录事务消息的本地事务状态,例如可以通过将消息唯一ID 存储在数据中, 并且该方法与业务代码处于同一个事务,与业务事务要么一起成功,要么一起失败。这里是事务消息设计的关键理念之一,为后续的事务状态回查提供唯一依据。
  • 如果消息发送失败,则设置本次事务状态为 Local TransactionStatc.ROLLBACK_MESSAGE。

image-20231001174810122

3.1.3、结束事务

Step3:结束事务。根据第二步返回的事务状态执行提交、回滚或暂时不处理事务。

  • LocalTransactionState.COMMIT_MESSAGE:提交事务。
  • LocalTransactionState.COMMIT MESSAGE:回滚事务。
  • LocalTransactionState.UNKNOW:结束事务,但不做任何处理。

image-20231001175045502

由于 this.end Transaction 的执行,其业务事务并没有提交,故在使用事务消息 TransactionListener #execute 方法除了记录事务消息状态后,应该返回LocalTransactionState.UNKNOW, 事务消息的提交与回滚通过下面提到的事务消息状态回查时再决定是否提交或回滚。

3.2、Broke 接受半事务消息

不要过分解读这个半事务消息,可以把它当做是一个普通消息,只不过broker在接收到这个普通消息时,由于它的属性中设置了事务的特性,所以会做些特殊处理,暂时不能被消息者消费,故叫半事务消息。

image-20231001181008521

Broker 端在收到消息存储请求时,如果消息为 prepare 消息,则执行 prepareMessage 方法,否则走普通消息的存储流程。

image-20231001181407269

这里是事务消息与非事务消息发送流程的主要区别,如果是事务消息则备份消息的原主题与原消息消费队列,然后将主题变更为 RMQ_SYS_TRANS HALF_TOPIC,消费队列变更为0,然后消息按照普通消息存储在 commitlog 文件进而转发到RMQ_SYS_TRANS_ HALF_ TOPIC 主题对应的消息消费队列。也就是说,事务消息在末提交之前并不会存入消息原有主题,自然也不会被消贵者消费。既然变更了主题,RocketMQ 通常会采用定时任务(单独的线程)去消费该主题,然后将该消息在满足特定条件下恢复消息主题,进而被消费者消费。读者对这种实现应该并不陌生,它与 RocketMQ 定时消息的处理过程如出一辙。

image-20231001181554540

这里就看到了ROCKETMQ常用的手段,就是替换topic. 首先备份原始消息的topic到属性中,然后替换原始topic为框架内部的topic:RMQ_SYS_TRANS_HALF_TOPIC

到这里,一阶段的过程结束了,Producer成功发送了一条半事务消息到broker,由于broker替换了topic,所以消费者暂时无法消费这条消息。

3.3、Broke接受本地事务状态

类比一阶段,二阶段就是这样子的:

image-20231015222622917

前面我们有分析过,本地事务执行完毕后,将会产生一个 LocalTransactionState 状态,然后Producer将状态信息发给Broker,Broker接收到这个状态后,根据状态做相应的处理。LocalTransactionState 状态 有三个值:COMMIT_MESSAGE, ROLLBACK_MESSAGEM, UNKNOW(将会引起回查),那么我们就根据这三种情况分别看下。

Broker端接收事务状态的处理器是:EndTransactionProcessor

3.3.1、本地事务状态:提交或回滚

如果本地事务提交,状态为:LocalTransactionState.COMMIT_MESSAGE,则Broker的处理逻辑:

image-20231015222925438

如果本地事务回滚,状态为:LocalTransactionState.ROLLBACK_MESSAGE,则Broker的处理逻辑:

image-20231015223329193

源码并不复杂,我们简单总结下:

  • 从commitlog中读取出Half消息.

    从结東事务请求命令中获取消息的物理偏移量( commitlogOffset),其实现逻辑由 TransactionalMessageService#。commitMessage 实现。

  • 将Half消息中备份的原始topic和queueid取出,并替换掉原来的Half消息的topic和queueid.

    恢复消息的主题、消费队列,构建新的消息对象,由 TransactionalMessageService#endMessageTransaction 实现。

  • 将替换后的消息再次写入commitlog中,此时消费者就可以消费了.

    将消息再次存储在 commitlog 文件中,此时的消息主题则为业务方发送的消息,将被转发到对应的消息消费队列,供消息消费者消费,其实现由 TransactionalMessage- Service#sendFinalMessage 实现。

  • 将半事务消息标记为删除.

    消息存储后,删除 prepare 消息,其实现方法并不是真正的删除,而是将 prepare 消息存储到 RMQ_SYS_TRANS_OP_HALF_TOPIC 主题中,表示该事务消息(prepare 状态的消息)已经处理过(提交或回滚),为未处理的事务进行事务回查提供查找依据。

3.3.2、本地事务状态:未知

EndTransactionProcessor 处理器中,我们看到了 COMMITROLLBACK, 但是并没有看到 UNKNOW 状态的处理逻辑,它实际上是通过异步线程TransactionalMessageCheckService 来完成处理的。

RocketMQ 通过 TransactionalMessageCheckService 线程定时去检测 RMQ SYS TRANS HALF TOPIC 主题中的消息,回查消息的事务状态。TransactionalMessageCheckService 的检测频率默认为1分钟,可通过在 broker.conf 文件中设置 transactionCheckInterval 来改变默认值,单位为毫秒。

image-20231015224255377

  • transactionTimeOut:事务的过期时间,只有当消息的存储时间加上过期时间大于系统当前时间时,才对消息执行事务状态回查,否则在下一次周期中执行事务回查操作。

  • transactionCheckMax:事务回查最大检测次数,如果超过最大检测次数还是无法获知消息的事务状态,RocketMQ 将不会继续对消息进行事务状态回查,而是直接丢弃即相当于回滚事务。

接下来重点分析 TransactionalMessageService#check 的实现逻辑

继续跟进去看下:

public class TransactionalMessageServiceImpl implements TransactionalMessageService {
  
    //TODO:.....省略其他代码.........

    @Override
    public void check(long transactionTimeout, int transactionCheckMax,
        AbstractTransactionalMessageCheckListener listener) {
        try {
            String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
            Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
           
            //TODO......略.........

            for (MessageQueue messageQueue : msgQueues) {
                long startTime = System.currentTimeMillis();
                MessageQueue opQueue = getOpQueue(messageQueue);
                long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
                long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
                
                //TODO......略.........

                List<Long> doneOpOffset = new ArrayList<>();
                HashMap<Long, Long> removeMap = new HashMap<>();
                PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);
               
                //TODO......略.........

                int getMessageNullCount = 1;
                long newOffset = halfOffset;
                long i = halfOffset;
                while (true) {
                    if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
                       
                         //TODO......略.........
                    }
                    if (removeMap.containsKey(i)) {
                         //TODO......略.........

                    } else {
                        GetResult getResult = getHalfMsg(messageQueue, i);
                        MessageExt msgExt = getResult.getMsg();
                        
                        //TODO......略.........

                       
                        List<MessageExt> opMsg = pullResult.getMsgFoundList();
                       
                        /**
                         * 什么样的消息需要回查?
                         * 1. 每一条半事务消息如果经过了commit/rollback, 那么对应就会创建一个Op消息,反过来说,如果没有Op消息,
                         *    但是有Half消息,那么说明是二阶段失败了(本地事务返回的状态码是UNKNOW),此时需要回查
                         *
                         * 2. 还有就是 TM在6秒内没有将最终确认状态发送给TC,此时也会触发回查
                         *
                         */
                        boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
                            || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
                            || (valueOfCurrentMinusBorn <= -1);

                        if (isNeedCheck) {
                            //TODO......略.........

                            //TODO:...."消息回查"
                            listener.resolveHalfMsg(msgExt);

                        } else {
                            //TODO......略.........
                        }
                    }
                    newOffset = i + 1;
                    i++;
                }
                if (newOffset != halfOffset) {
                    transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
                }
                long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
                if (newOpOffset != opOffset) {
                    transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
                }
            }
        } catch (Throwable e) {
            log.error("Check error", e);
        }

    }

 
    //TODO:.......省略其他代码...................
}

什么样的消息需要回查?

  1. 每一条半事务消息如果经过了commit/rollback, 那么对应就会创建一个Op消息,反过来说,如果没有Op消息,但是有Half消息,那么说明是二阶段失败了(本地事务返回的状态码是UNKNOW),此时需要回查.
  2. TM(也就是Producer)在6秒(默认6s)内没有将最终确认状态发送给TC(Broker),此时也会触发回查.

那接下来我们就看下回查的整体过程:

3.3.2.1、Broker发起消息回查
public void resolveHalfMsg(final MessageExt msgExt) {
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                sendCheckMessage(msgExt);
            } catch (Exception e) {
                LOGGER.error("Send check message error!", e);
            }
        }
    });
}

继续跟进去:

public void sendCheckMessage(MessageExt msgExt) throws Exception {
    CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader();
    
    //TODO:.....设置属性......
    
    Channel channel = brokerController.getProducerManager().getAvailableChannel(groupId);
    if (channel != null) {
       //TODO: Broker 给 Client 发送消息
       brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt);
    } else {
        LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId);
    }
}

继续跟进去:

public void checkProducerTransactionState(
    final String group,
    final Channel channel,
    final CheckTransactionStateRequestHeader requestHeader,
    final MessageExt messageExt) throws Exception {
    RemotingCommand request =
    //TODO: code:RequestCode.CHECK_TRANSACTION_STATE
    RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader);
    request.setBody(MessageDecoder.encode(messageExt, false));
    try {
        //TODO: 发送给生产者
        this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
    } catch (Exception e) {
        log.error("Check transaction failed because invoke producer exception. group={}, msgId={}, error={}",
                group, messageExt.getMsgId(), e.toString());
    }
}

到这里,Broker就将回查的请求发送给了Producer, 那么我们接下来就看下Producer 作何处理?

3.3.2.1、Producer 处理消息回查

Producer接收消息回查请求的是 ClientRemotingProcessor 类:

public class ClientRemotingProcessor extends AsyncNettyRequestProcessor implements NettyRequestProcessor {
    
    //TODO:......略.......
    
    @Override
    public RemotingCommand processRequest(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {
        switch (request.getCode()) {
            case RequestCode.CHECK_TRANSACTION_STATE:
                return this.checkTransactionState(ctx, request);
            
            //TODO:.....忽略其他case.......
            default:
                break;
        }
        return null;
    }
    
     //TODO:......略........
}    

继续跟进去,来到 DefaultMQProducerImpl类的checkTransactionState(...)方法中,这方法内容比较多,但其实主要就做了3件事:

  1. 获取本地事务监听器(就是代码实例中指定的MyLocalTransactionCheckListener)
  2. 执行本地事务的回查
  3. 将回查结果上报给Broker
@Override
public void checkTransactionState(final String addr, final MessageExt msg,
    final CheckTransactionStateRequestHeader header) {
    Runnable request = new Runnable() {
        private final String brokerAddr = addr;
        private final MessageExt message = msg;
        private final CheckTransactionStateRequestHeader checkRequestHeader = header;
        private final String group = DefaultMQProducerImpl.this.defaultMQProducer.getProducerGroup();

        @Override
        public void run() {
            TransactionCheckListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener();
            
            //TODO:1.获取事务监听器,就是demo中我们指定d的MyLocalTransactionCheckListener
            TransactionListener transactionListener = getCheckListener();
            if (transactionCheckListener != null || transactionListener != null) {
                LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
                
                //TODO: 2. 执行本地事务回查(省略其他if判断代码)
                localTransactionState = transactionListener.checkLocalTransaction(message);

                this.processTransactionState(
                    localTransactionState,
                    group,
                    exception);
            } else {
                log.warn("CheckTransactionState, pick transactionCheckListener by group[{}] failed", group);
            }
        }

        //TODO:这个方法和前面的endTransaction(....)方法的逻辑是一模一样的,就是将回查后的状态发给broker
        private void processTransactionState(
            final LocalTransactionState localTransactionState,
            final String producerGroup,
            final Throwable exception) {
            
            //TODO:.....省略诸多代码..........

            try {
               //TODO:3. 将回查结果上报给Broker
               DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark,
                    3000);
            } catch (Exception e) {
                log.error("endTransactionOneway exception", e);
            }
        }
    };

    //TODO: 这个 checkExecutor 就是demo 示例中我们指定的线程池
    this.checkExecutor.submit(request);
}

其中回查的逻辑就在我们定义的事务监听器 MyLocalTransactionCheckListener 中:

/**
 * 本地事务监听器
 *
 * @author qiuguan
 */
public class LocalTransactionCheckListener implements TransactionListener {

    //TODO:.....省略本地事务逻辑......


    /**
     * tagC 的本地事务状态是 UNKNOW, 所以会引起回查
     * 回查后是 COMMIT_MESSAGE, 那么则最终会提交该消息,被消费者看到消费
     * @param msg
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("topic = " + msg.getTopic() + ", tag = " + msg.getTags() + " 消息回查");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

然后将回查后的状态,再次发送给Broker

DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark,
                    3000);

到这里,其实整个事务消息的流程就结束了,我们可以通过一张简图看下:

image-20231022225630438

四、事务消息的架构思考

事务消息能保证业务与消息发送这两个操作的强一致性,以前在没有事务消息时,通常有两种方式解决方案。

  • 严格的事务一致性,采用与 RocketMQ 事务消息的实现模型,自己实现本地事务表与回调,即多了一个步骤,通过定时任务扫描本地事务消息,进行消息发送。
  • 基于补偿的思想,例如只在消息发送时采用消息重试机制,确保消息发送成功,另外结合业务的状态,以订单流为例,订单状态为已成功支付后,向 RocketMQ 集群发送一条消息,然后商户物流系统订阅该 Topic,对其进行消费,处理完后会将订单的状态变更为已发货,但如果这条消息被丢失,那无法驱动订单的后续流程,故这里可以基于补偿思想,用一个定时器扫描订单表,查找那些已支付但未发货的订单,并且已超过多少时间的订单,补发一条消息,同样能够最终的一致性。

事务消息确实能提供强一致性,但需要引入事务本地表,每一次业务都需要增加一次数据库的写入开销,而基于补偿思路,采取的是乐观的机制,并且出错的概率本来就很低,故效率通常会更好。

故大家可以根据实际情况进行技术选型,不要觉得事务消息这项技术和牛,就必须选用此种方案。

五、总结

事务消息实现原理如下图所示:

image-20231022230204764

应用程序在事务内完成相关业务数据落库后,需要同步调用 RocketMQ 消息发送接口,发送状态为 prepare 的消息,消息发送成功后 RocketMQ 服务器会回调 RocketMQ 消息发送者的事件监听程序,记录消息的本地事务状态,该相关标记与本地业务操作同属一个事务,确保消息发送与本地事务的原子性。

RocketMQ 在收到类型为 prepare 的消息时,会首先备份消息的原主题与原消息消费队列,然后将消息存储在主题为 RMQ_SYS_TRANS_HALF_TOPIC 的消息消费队列中,就是因为这样,消费端并不会立即被消费到。

RocketMQ 消息服务器开启一个定时任务,消费 RMQ_SYS_TRANS_HALF_TOPIC 的消息,向消息发送端(应用程序)发起消息事务状态回查,应用程序根据保存的事务状态回馈消息服务器事务的状态(提交、回滚、未知),如果是提交或回滚,则消息服务器提交或回滚消息,如果是未知,待下一次回查,RocketMQ 允许设置一条消息的回查间隔与回查次数,如果在超过回查次数后未知消息的事务状态,则默认回滚消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小颜-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值