RocketMQ之事务消息

500 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一 理论

RocketMQ 的事务消息,是指发送消息事件和其他事件需要同时成功或同时失败。 一个常见的场景:订单付款后给用户发优惠券,订单系统需要作为生产者发消息,优惠券系统作为消费者收消息。 在这个场景下,发送“用户已付款” 消息,要和“用户付款”这个操作同时成功或者同时失败。

RocketMQ 采用两阶段提交的方式实现事务消息。 TransactionMQProducer处理上面情况的流程是,先发一个“用户已付款”的半消息,发送成功后做用户付款的操作,根据操作结果是否成功,确定之前的“用户已付款”的消息是做commit 还是rollback。具体流程如下图所示:

image2021-10-6_22-42-40.png

严格的事务实现,需要实现ACID,那么rocketmq都实现了吗?还以【订单付款后给用户发优惠券】场景说明:

A(原子性):付款和发券同时发生或不发生,实现了原子性。

C(一致性):实现了最终一致性,在上图步骤4或者7未完成前是不一致的。

I(隔离性):存在读未提交的可能,因此未实现隔离性。

D(持久性):消息最后都会落盘,实现了持久性。

二 实战

(1)Create the transactional producer

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 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(transactionListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg =
                    new Message("TopicTest1234", 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();
    }
}

(2)Implement the TransactionListener interface

import ...

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;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

三 参考

Transaction example

《RocketMQ实战与原理解析》杨开元 3.2.4