参考:https://www.jianshu.com/p/5248abc8b8ed

一、场景:账户A向账户B付款100元,A账户需要减100元,B账户需要加100元。A、B是两个不同的系统。这时就不能用传统的本地数据库事务解决了。采用消息中间件,如果A账户先扣款,然后发送消息可能出现A扣款成功,消息发送失败,A账户减了,B账户却没有加钱。如果先发送消息,A再扣款可能出现B消费了消息账户加了,但是A扣款失败。因此分布式消息需要解决的问题就是A账户变动和发送消息的原子性,即解决 Producer 端的消息发送与本地事务执行的原子性问题。

二、RocketMQ 事务消息

RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题。
RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制,则为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计,则为事务消息在系统在发生异常时,依然能够保证事务的最终一致性达成。

三、RocketMQ 事务消息设计

事务消息作为一种异步确保型事务, 将两个事务分支通过 MQ 进行异步解耦,RocketMQ 事务消息的设计流程同样借鉴了两阶段提交理论,整体交互流程如下图所示:

 
image

 

  • 事务发起方(即消息发送者)首先发送 prepare 消息到 MQ。
  • 事务发起方(即消息发送者)在发送 prepare 消息成功后执行本地事务。
  • 根据本地事务执行结果发送 commit 或者是 rollback 给 MQ。
    • 如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发。
    • 如果消息是 commit,MQ 将会把这个消息发送给 consumer 端。
  • 如果执行本地事务过程中,执行端挂掉,或者超时,导致 MQ 收不到任何的消息(不知道是该 commit 还是该 rollback),RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了某个 prepare 消息还不知道该怎么处理,它会向消息发送者确认,所以消息发送者需要实现一个 check 接口,RocketMQ 会根据消息发送者设置的策略来决定是 rollback 还是继续 commit。这样就保证了消息发送与本地事务同时成功或同时失败。
  • Consumer 端的消费成功机制由 MQ 保证。
 
image
四、RocketMQ 事务消息实现

在具体实现上,RocketMQ 通过使用 Half Topic 以及 Operation Topic 两个内部队列来存储事务消息推进状态,如下图所示:


 
image

其中,Half Topic 对应队列中存放着 prepare 消息,Operation Topic 对应的队列则存放了 prepare message 对应的 commit/rollback 消息,消息体中则是 prepare message 对应的 offset,服务端通过比对两个队列的差值来找到尚未提交的超时事务,进行回查。

从用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可;而在 service 层,则对事务消息的两阶段提交进行了抽象,同时针对超时事务实现了回查逻辑,通过不断扫描当前事务推进状态,来不断反向请求 Producer 端获取超时事务的执行状态,在避免事务挂起的同时,也避免了 Producer 端的单点故障。
而在存储层,RocketMQ 通过 Bridge 封装了与底层队列存储的相关操作,用以操作两个对应的内部队列,用户也可以依赖其他存储介质实现自己的 service,RocketMQ 会通过 ServiceProvider 加载进来。

从上述事务消息设计中可以看到,RocketMQ 事务消息较好的解决了事务的最终一致性问题,事务发起方仅需要关注本地事务执行以及实现回查接口给出事务状态判定等实现,而且在上游事务峰值高时,可以通过消息队列,避免对下游服务产生过大压力。

package com.ord.rocketmq.quickstart;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;

public class TransactionProducer {
    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer(
                "quickstart_producer");
        producer.setNamesrvAddr("192.168.87.130:9876;192.168.87.131:9876;192.168.87.132:9876;192.168.87.133:9876");
        producer.start();
        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);
        TransactionListener transactionListener = new MyTransactionListener();
        producer.setTransactionListener(transactionListener);
        
        for (int i=1; i<=1; i++) {
            try {
                Message msg = new Message("TopicTransaction", "Tags" + i, "Keys" + i,
                        ("Hello RocketMq" + i).getBytes());
                TransactionSendResult sendResult = producer
                        .sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}
package com.ord.rocketmq.quickstart;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

public class MyTransactionListener implements TransactionListener{
    private AtomicInteger transactionIndex = new AtomicInteger(1);
    
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<String, Integer>();
    
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Integer index = transactionIndex.getAndIncrement();
        index = index % 3;
        localTrans.put(msg.getTransactionId(), index);
        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;
    }

}