一、问题引入

这里的一个需求场景是:工行用户 A 向建行用户 B 转账 1 万元。

我们可以使用同步消息来处理该需求场景:

  1. 工行系统发送一个给 B 增款 1 万元的同步消息 M 给 Broker
  2. 消息被 Broker 成功接收后,向工行系统发送成功 ACK
  3. 工行系统收到成功 ACK 后从用户 A 中扣款 1 万元
  4. 建行系统从 Broker 中获取到消息 M
  5. 建行系统消费消息 M,即向用户 B 中增加 1 万元

这其中是有问题的:若第 3 步中的扣款操作失败,但消息已经成功发送到了 Broker。对于 MQ 来说,只要消息写入成功,那么这个消息就可以被消费。此时建行系统中用户 B 增加了 1 万元。出 现了数据不一致问题。

二、解决思路

解决思路是,让第 1、2、3 步具有原子性,要么全部成功,要么全部失败。即消息发送成功后,必须要保证扣款成功。如果扣款失败,则回滚发送成功的消息。而该思路即使用事务消息。这里要使用分布式事务解决方案

使用事务消息来处理该需求场景:

  1. 事务管理器 TM 向事务协调器 TC 发起指令,开启全局事务
  2. 工行系统发一个给 B 增款 1 万元的事务消息 M 给 TC
  3. TC 会向 Broker 发送半事务消息 prepareHalf,将消息 M预提交到 Broker。此时的建行系统是看不到 Broker 中的消息 M 的
  4. Broker 会将预提交执行结果 Report 给 TC。
  5. 如果预提交失败,则 TC 会向 TM 上报预提交失败的响应,全局事务结束;如果预提交成功,TC 会调用工行系统的回调操作,去完成工行用户 A 的预扣款1 万元的操作
  6. 工行系统会向 TC 发送预扣款执行结果,即本地事务的执行状态
  7. TC 收到预扣款执行结果后,会将结果上报给 TM。

    预扣款执行结果存在三种可能性:

// 描述本地事务执行状态
public enum LocalTransactionState {
  COMMIT_MESSAGE, // 本地事务执行成功
  ROLLBACK_MESSAGE, // 本地事务执行失败
  UNKNOW, // 不确定,表示需要进行回查以确定本地事务的执行结果
}
  1. TM 会根据上报结果向 TC 发出不同的确认指令
  • 若预扣款成功(本地事务状态为 COMMIT_MESSAGE),则 TM 向 TC 发送 Global Commit 指令
  • 若预扣款失败(本地事务状态为 ROLLBACK_MESSAGE),则 TM 向 TC 发送 Global Rollback 指令
  • 若现未知状态(本地事务状态为 UNKNOW),则会触发工行系统的本地事务状态回查操作。回查操作会将回查结果,即 COMMIT_MESSAGE 或 ROLLBACK_MESSAGE Report 给 TC。TC 将结果上报给 TM,TM 会再向 TC 发送最终确认指令 Global Commit 或 Global Rollback
  1. TC 在接收到指令后会向 Broker 与工行系统发出确认指令
  • TC 接收的若是 Global Commit 指令,则向 Broker 与工行系统发送 Branch Commit 指令。此时 Broker 中的消息 M 才可被建行系统看到;此时的工行用户 A 中的扣款操作才真正被确认
  • TC 接收到的若是 Global Rollback 指令,则向 Broker 与工行系统发送 Branch Rollback 指令。此时 Broker 中的消息 M 将被撤销;工行用户 A 中的扣款操作将被回滚

以上方案就是为了确保消息投递与扣款操作能够在一个事务中,要成功都成功,有一个失败,则全部回滚。

以上方案并不是一个典型的 XA 模式。因为 XA 模式中的分支事务是异步的,而事务消息方案中的消息预提交与预扣款操作间是同步的。

三、基础

分布式事务

对于分布式事务,通俗地说就是,一次操作由若干分支操作组成,这些分支操作分属不同应用,分布在不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。分布式事务与普通事务一样,就是为了保证操作结果的一致性。

事务消息

RocketMQ 提供了类似 X/Open XA 的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA 是一种分布式事务解决方案,一种分布式事务处理模式。

半事务消息

暂不能投递的消息,发送方已经成功地将消息发送到了 Broker,但是 Broker 未收到最终确认指令,此时该消息被标记成"暂不能投递"状态,即不能被消费者看到。处于该种状态下的消息即半事务消息。

本地事务状态

Producer回调操作执行的结果为本地事务状态,其会发送给 TC,而 TC 会再发送给 TM。TM 会根据 TC 发送来的本地事务状态来决定全局事务确认指令。

// 描述本地事务执行状态
public enum LocalTransactionState {
  COMMIT_MESSAGE, // 本地事务执行成功
  ROLLBACK_MESSAGE, // 本地事务执行失败
  UNKNOW, // 不确定,表示需要进行回查以确定本地事务的执行结果
}

消息回查

消息回查,即重新查询本地事务的执行状态。本例就是重新到 DB 中查看预扣款操作是否执行成功。

注意,消息回查不是重新执行回调操作。回调操作是进行预扣款操作,而消息回查则是查看预扣款操作执行的结果。

引发消息回查的原因最常见的有两个:

1)回调操作返回 UNKNWON

2)TC 没有接收到 TM 的最终全局事务确认指令

RocketMQ 中的消息回查设置

关于消息回查,有三个常见的属性设置。它们都在 broker 加载的配置文件中设置,例如:

  • transactionTimeout=20,指定 TM 在 20 秒内应将最终确认状态发送给 TC,否则引发消息回查。默认为 60 秒
  • transactionCheckMax=5,指定最多回查 5 次,超过后将丢弃消息并记录错误日志。默认 15 次。
  • transactionCheckInterval=10,指定设置的多次消息回查的时间间隔为 10 秒。默认为 60 秒。

四、XA 模式三剑客

XA 协议

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

XA 模式中有三个重要组件:TC、TM、RM。

TC

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

RocketMQ 中 Broker 充当着 TC。

TM

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

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

RM

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

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

五、XA 模式架构

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

  1. TM 向 TC 发起指令,开启一个全局事务。
  2. 根据业务要求,各个 RM 会逐个向 TC 注册分支事务,然后 TC 会逐个向 RM 发出预执行指令。
  3. 各个 RM 在接收到指令后会在进行本地事务预执行。
  4. RM 将预执行结果 Report 给 TC。当然,这个结果可能是成功,也可能是失败。
  5. TC 在接收到各个 RM 的 Report 后会将汇总结果上报给 TM,根据汇总结果 TM 会向 TC 发出确认指令。
  • 若所有结果都是成功响应,则向 TC 发送 Global Commit 指令。
  • 只要有结果是失败响应,则向 TC 发送 Global Rollback 指令。
  1. TC 在接收到指令后再次向 RM 发送确认指令。

事务消息方案并不是一个典型的 XA 模式。因为 XA 模式中的分支事务是异步的,而事务消息方案中的消息预提交与预扣款操作间是同步的。

六、注意

  • 事务消息不支持延时消息
  • 对于事务消息要做好幂等性检查,因为事务消息可能不止一次被消费(因为存在回滚后再提交的情况)

七、代码举例

定义工行事务监听器

public class ICBCTransactionListener implements TransactionListener {
    // 回调操作方法
    // 消息预提交成功就会触发该方法的执行,用于完成本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("预提交消息成功:" + msg);
        // 假设接收到TAGA的消息就表示扣款操作成功,TAGB的消息表示扣款失败,
        // TAGC表示扣款结果不清楚,需要执行消息回查
        if (StringUtils.equals("TAGA", msg.getTags())) {
            return LocalTransactionState.COMMIT_MESSAGE;
        } else if (StringUtils.equals("TAGB", msg.getTags())) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        } else if (StringUtils.equals("TAGC", msg.getTags())) {
            return LocalTransactionState.UNKNOW;
        }
        return LocalTransactionState.UNKNOW;
    }
    //消息回查方法
    // 引发消息回查的原因最常见的有两个:
    // 1)回调操作返回UNKNWON
    // 2)TC没有接收到TM的最终全局事务确认指令
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("执行消息回查" + msg.getTags());
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

定义事物消息生产者

public class TransactionProducer {
    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer("tpg");
        producer.setNamesrvAddr("rocketmqOS:9876");
        /**
         * 定义一个线程池
         * @param corePoolSize 线程池中核心线程数量
         * @param maximumPoolSize 线程池中最多线程数
         * @param keepAliveTime 这是一个时间。当线程池中线程数量大于核心线程数量是多余空闲线程的存活时长
         * @param unit 时间单位
         * @param workQueue 临时存放任务的队列,其参数就是队列的长度
         * @param threadFactory 线程工厂
         */

        ExecutorService executorService = new ThreadPoolExecutor(25,
                100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(2000), new
                ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r);
                        thread.setName("client-transaction-msg-check-thread");
                        return thread;
                    }
                });
        // 为生产者指定一个线程池
        producer.setExecutorService(executorService);
        // 为生产者添加事务监听器
        producer.setTransactionListener(new ICBCTransactionListener());
        producer.start();
        String[] tags = {"TAGA""TAGB""TAGC"};
        for (int i = 0; i < 3; i++) {
            byte[] body = ("Hi," + i).getBytes();
            Message msg = new Message("TTopic", tags[i], body);
            // 发送事务消息
            // 第二个参数用于指定在执行本地事务时要使用的业务参数
            SendResult sendResult = producer.sendMessageInTransaction(msg, null);
            System.out.println("发送结果为:" + sendResult.getSendStatus());
        }
    }
}

定义消费者

直接使用普通消息的 SomeConsumer 作为消费者即可。

public class SomeConsumer {
    public static void main(String[] args) throws MQClientException {
        // 定义一个pull消费者
        // DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("cg");
        // 定义一个push消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
        // 指定nameServer
        consumer.setNamesrvAddr("rocketmqOS:9876");
        // 指定从第一条消息开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // 指定消费topic与tag
        consumer.subscribe("TTopic""*");
        // 指定采用“广播模式”进行消费,默认为“集群模式”
        // consumer.setMessageModel(MessageModel.BROADCASTING);
        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            // 一旦broker中有了其订阅的消息就会触发该方法的执行,
            // 其返回值为当前consumer消费的状态
             @Override
             public ConsumeConcurrentlyStatus
             consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) 
{
                 // 逐条消费消息
                 for (MessageExt msg : msgs) {
                     System.out.println(msg);
                 }
                 //返回消费状态:消费成功
                 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
             }
         });
        // 开启消费者消费
        consumer.start();
        System.out.println("Consumer Started");
    }
}
posted on 2022-08-14 17:47  格物致知_Tony  阅读(69)  评论(0编辑  收藏  举报