RocketMQ实现分布式事务

252 阅读5分钟

1、背景:

RockeyMQ实现分布式事务(拿下单和扣减库存为例,如果下单和扣减库存在一个分布式系统中,需要维持一个事务性)

如果下单后发消息提示需要扣减库存,保持一个分布式事务的一致性,就会出现以下场景:

  • 场景一:先下单后发消息

    问题:先下单后如果消息发送失败,无法扣减库存业务无法订阅了此消息,但是却下了单;

  • 场景二:先发消息后下单

    问题:消息发送成功,但是下单失败,但是扣减库存的业务订阅了此消息,则扣减了库存却未下单。

    RocketMQ解决此问题的方法分为两个阶段:prepared阶段和确认阶段(其中确认阶段分为CommitRollback,取决于事务是否执行成功)。

2、流程分析

prepared阶段:主要发一个消息到Rocketmq,但该消息只储存在commitlogconsumeQueue中不可见,也就是消费端(订阅端)无法看到此消息

确认阶段:主要是把prepared消息保存到consumeQueue,即让消费端可以看到此消息,也就是可以消费此消息

上图说明了事务消息的大致方案,其中分为两种情况:正常事务消息的发送及提交、事务消息的补偿流程。

2.1、事务消息发送及提交

(1) 发送消息(half消息);

(2) 服务端响应消息写入结果;

(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行);

(4) 根据本地事务状态执行Commit或者RollbackCommit操作生成消息索引,消息对消费者可见)。

2.2、事务补偿

(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”;

(2) Producer收到回查消息,检查回查消息对应的本地事务的状态;

(3) 根据本地事务状态,重新Commit或者Rollback;

其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

​ 事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息;
  • TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费;
  • TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。

​ 若本地事务执行了很多张表,那是不是我们要把那些表都要进行判断是否执行成功呢?这样是不是太麻烦了,而且和业务很耦合。

有没有更好的方式呢?就是设计一张Transaction,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功时,Transaction中应当已经记录该TransactionId的状态为「已完成」。当RocketMQ回查时,只需要检查对应的**TransactionId的状态是否是「已完成」就好**,而不用关心具体的业务数据。

3、发送事务消息

3.1、创建事务性生产者

使用 TransactionMQProducer类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需要根据执行结果对消息队列进行回复。回传的事务状态在请参考前一节。

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        //创建事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        //创建消息生产者
        TransactionMQProducer producer = new TransactionMQProducer("group6");
        producer.setNamesrvAddr("192.168.25.135:9876;192.168.25.138:9876");
        //生产者这是监听器
        producer.setTransactionListener(transactionListener);
        //启动消息生产者
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        for (int i = 0; i < 3; i++) {
            try {
                Message msg = new Message("TransactionTopic", 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);
                TimeUnit.SECONDS.sleep(1);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        //producer.shutdown();
    }
}

3.2、实现事务的监听接口

当发送消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTranscation 方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。

public class TransactionListenerImpl implements TransactionListener {

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("执行本地事务");
        if (StringUtils.equals("TagA", msg.getTags())) {
            return LocalTransactionState.COMMIT_MESSAGE;
        } else if (StringUtils.equals("TagB", msg.getTags())) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        } else {
            return LocalTransactionState.UNKNOW;
        }

    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("MQ检查消息Tag【"+msg.getTags()+"】的本地事务执行结果");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

4、使用限制与注意事项

(1) 事务消息不支持延时消息和批量消息。

(2)为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。

(3)事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。

(4)事务性消息可能不止一次被检查或消费。

(5)提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。

(6)事务消息的生产者 ID 不能与其他类型消息的生产者ID共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者ID 查询到消费者。