RocketMQ学习笔记-事务性

RocketMQ学习笔记-事务性


一、前言

我们经常用支付宝来付钱/转账,这是日常生活的一件普通小事,但是我们思考支付宝扣除支付/转账的钱之后,如果系统挂掉怎么办,这时对方账户并没有增加相应的金额,数据就会出现不一致状况了。这种问题本质上可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。如果在单机系统(数据库实例也在一个系统)上,我们可以用本地事务轻松解决;但现在系统多为微服务架构,之间通过MQ传递消息,这时候就需要保证数据库事务和MQ事务的一致性。
我们以微服务架构的购物场景为例,参照一下RocketMQ官方的例子,用户A发起订单,支付100块钱操作完成后,能得到100积分,账户服务和会员服务是两个独立的微服务模块,有各自的数据库,可能出现以下情况:

  • 如果先扣款,再发消息,可能钱刚扣完,宕机了,消息没发出去,结果积分没增加。
  • 如果先发消息,然后再扣款,可能积分增加了,但钱没扣掉,白送人家100积分。
  • 钱正常扣了,消息也发送成功了,但会员服务实例消费消息出现问题,结果积分没增加。
    转账场景

https://www.cnblogs.com/huangying2124/p/11702761.html

RocketMQ事务消息解决的是本地(数据库)事务执行与消息发送的原子性问题,是确保MQ生产端正确无误地将消息发送出来,没有多发,也不会漏发。但至于发送后消费端有没有正常的消费掉(如上面提及的第三种情况,钱正常扣了,消息也发了,但下游消费出问题导致积分不对),这种异常场景将由MQ消息消费失败重试机制来保证,不在此次的讨论范围内。

二、样例分析

以RocketMQ提供的demo为参考。与普通的消息发送过程不同,事务消息多实现一个事务消息监听器。

2.1 创建事务消息监听器

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

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

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

    /**
     * 回查本地事务结果
     *
     * @param msg Check message
     * @return
     */
    @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;
    }
}

事务消息监听器需要继承TransationLister接口,并实现两个回调方法。

  • 实现executeLocalTransaction(本地事务逻辑)方法,当prepare(half)消息提交成功后,执行该方法,执行完毕后返回本地事务结果状态码给Broker,其中状态分为三种:
    • LocalTransactionState.COMMIT_MESSAGE 本地事务执行成功状态,prepare消息继续提交;
    • LocalTransactionState.ROLLBACK_MESSAGE 本地事务执行失败状态,prepare消息需要回滚;
    • LocalTransactionState.UNKNOW 本地事务执行状态未知,此时broker处于蒙蔽状态,需要启动回查机制,查一下本地事务到底执行成功还是失败。
  • 实现checkLocalTransaction(事务回查逻辑)方法,当Rokcet Broker端不清楚本地事务的执行状态时(返回的是未知状态),会启动回查机制,就会走到该方法,执行完毕后返回本地事务结果状态。

2.2 创建消息生产者

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 构造事物消息的生产者
        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);
        // 当RocketMQ发现`Prepared消息`时,会根据这个Listener实现来决策事务
        TransactionListener transactionListener = new TransactionListenerImpl();
        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();
    }
}

可以看到,事务消息和普通消息的发送过程大体相似,区别有两处:

  • 设置本地事务监听器,也就是自定义的TransationListener的实现类,后续可以通过匿名内部类或者lambda表达式的方式来实现TransationListener接口。
  • 设置了执行本地回查事务状态的线程池,这个线程池会在Broker调用回查机制后触发使用。如果不自己定义的,会有一个默认数量为1的线程池(org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#initTransactionEnv)。
    以上只是一个例子的使用方法,我们同时也可以思考几个问题:
  • 事务消息发送者TransactionMQProducer和普通的DefaultMQProducer有什么区别?
  • 事务消息是如何与Borker交互的?
  • 是怎么样实现回查机制的,并调用TransactionListener实现类?

带着这些问题,下面将具体展开说明。

三、事务消息设计思路

RocketMQ事务消息通过异步确保方式,保证事务的最终一致性。设计流程上借鉴两阶段提交理论,流程图如下所示。
事务消息设计思路

https://www.cnblogs.com/huangying2124/p/11702761.html

  1. 应用模块遇到发送事务消息的场景,先发送prepare消息给MQ,Broker端存放在RMQ_SYS_TRANS_HALF_TOPIC主题下。
  2. perpare消息发送成功后,应用模块执行本地事务方法TransactionListener#executeLocalTransaction。
  3. 将本地事务的执行结果状态,再返回Commit或Rollback给MQ。
  4. 如果是Commit,MQ会直接把消息发给consumer;如果是Rollback,则直接删除prepare消息。
  5. 第3步的执行结果如果没响应,或是超时的,启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息),处理结果同第4步。
  6. 至于消费是否成功,需要自己保证。可以通过自动重试来实现。

四、事务消息具体实现

4.1 消息发送过程

首先回答第一个问题,事务消息发送者TransactionMQProducer继承于DefaultMQProducer,并加入了两个关键信息,这在前面也提到过:

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

4.1.1 发送者

我们都知道,RocketMQ的事务是通过两阶段提交来实现的,先来看第一阶段,发送prepared半消息。
TransactionMQProducer发送事务消息是调用org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction函数。

// 检查监听器
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
  throw new MQClientException("tranExecutor is null", null);
}

// ignore DelayTimeLevel parameter
if (msg.getDelayTimeLevel() != 0) {
  MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}

Validators.checkMessage(msg, this.defaultMQProducer);

SendResult sendResult = null;
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
  // 发送
  sendResult = this.send(msg);
} catch (Exception e) {
  throw new MQClientException("send message Exception", e);
}

事务消息发送前,需要一些准备工作。首先检查事件监听器,如果为空则直接抛异常;然后清除延迟消息标记,事务消息是不支持延迟消息的;最后为消息添加属性,TRANSACTION_PREPARED和GROUP,分别表示消息为prepared消息和消息所属的消息生产者组。设置消息生产者组的目的是在回查本地事务状态的时候,从该生产者组中随机选取一个消息生产者。

LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
  case SEND_OK: {
	try {
	  if (sendResult.getTransactionId() != null) {
		msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
	  }
	  String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
	  if (null != transactionId && !"".equals(transactionId)) {
		msg.setTransactionId(transactionId);
	  }
	  if (null != localTransactionExecuter) {
		localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
	  } else if (transactionListener != null) {
		log.debug("Used new transaction API");
		localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
	  }
	  if (null == localTransactionState) {
		localTransactionState = LocalTransactionState.UNKNOW;
	  }

	  if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
		log.info("executeLocalTransactionBranch return {}", localTransactionState);
		log.info(msg.toString());
	  }
	} catch (Throwable e) {
	  log.info("executeLocalTransactionBranch exception", e);
	  log.info(msg.toString());
	  localException = e;
	}
  }
	break;
  case FLUSH_DISK_TIMEOUT:
  case FLUSH_SLAVE_TIMEOUT:
  case SLAVE_NOT_AVAILABLE:
	localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
	break;
  default:
	break;
}

发送完成后,根据发送结果来执行相应操作:

  • 消息发送成功,会去执行本地事务,此时需要去找transactionListener,并用localTransactionState记录本地事务的执行状态结果。
  • 消息发送失败,比如超时、不可用等原因。则设置本次事务状态为LocalTransactionState.ROLLBACK_MESSAGE。

此时,半消息发送接受完成,进入了第二阶段,提交或回滚事务。

try {
  this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
  log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}

根据上一步的本地事务状态执行提交、回滚或暂时不处理事务:

  • LocalTransactionState.COMMIT_MESSAGE:提交事务;
  • LocalTransactionState.ROLLBACK_MESSAGE:回滚事务;
  • LocalTransactionState.UNKNOW:结束事务,但不做任何操作处理,就是不作为。
    具体的处理是需要发送拥有事务状态的请求给Broker端,体现在endTransaction()函数中。
final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
requestHeader.setTransactionId(transactionId);
requestHeader.setCommitLogOffset(id.getOffset());
switch (localTransactionState) {
  case COMMIT_MESSAGE:
	requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
	break;
  case ROLLBACK_MESSAGE:
	requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
	break;
  case UNKNOW:
	requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
	break;
  default:
	break;
}
this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
															   this.defaultMQProducer.getSendMsgTimeout());

至此,事务消息发送完毕。

4.1.2 Broker

上文中,发送者的任务完成了,Broker作为消息接收/存储方,需要看一下处理过程。
首先是第一阶段的半消息发送。Broker端在接收到消息存储时,如果判断是prepare消息,则执行prepareMessage方法,否则走普通消息的存储过程。

// prepareMessage调用该方法
public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
  return store.putMessage(parseHalfMessageInner(messageInner));
}

private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
  MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
  MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
							  String.valueOf(msgInner.getQueueId()));
  msgInner.setSysFlag(
	MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
  // 设置topic为RMQ_SYS_TRANS_HALF_TOPIC
  msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
  msgInner.setQueueId(0);
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
  return msgInner;
}

事务消息和普通消息存储的区别在于,事务消息是直接存储到RMQ_SYS_TRANS_HALF_TOPIC主题下,不会存入到消息原有主题。这样,事务消息自然不会被消费。那什么时候消费呢?是在本地事务状态为提交的情况下,会被移到原有消息队列,自然而然的也就会被消费,这就是后面要讲的了,第二阶段的,事务状态提交/回滚。
Broker服务端的结束事务处理器为EndTransactionProcessor。

OperationResult result = new OperationResult();
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
  // 提取prepare消息
  result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
  if (result.getResponseCode() == ResponseCode.SUCCESS) {
	RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
	if (res.getCode() == ResponseCode.SUCCESS) {
	  MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
	  msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
	  msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
	  msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
	  msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
	  MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
	  // 存储至commitlog文件中
	  RemotingCommand sendResult = sendFinalMessage(msgInner);
	  if (sendResult.getCode() == ResponseCode.SUCCESS) {
		// 删除半消息
		this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
	  }
	  return sendResult;
	}
	return res;
  }
} else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
  result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
  if (result.getResponseCode() == ResponseCode.SUCCESS) {
	RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
	if (res.getCode() == ResponseCode.SUCCESS) {
	  this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
	}
	return res;
  }
}

Broker服务端对提交的本地事务动作的处理态度如下:

  • 若为提交事务,则先根据物理偏移量获取prepare消息,然后恢复消息的主题和消费队列,构建新的信息对象,重新存储到commitlog文件中,这次消息主题是业务方的原有消费主题,也将被转发到对应的消息消费队列,供消费者消费。消息存储后,删除prepare消息,这时并不是真正的删除,而是将其移入RMQ_SYS_TRANS_OP_HALF_TOPIC主题中,表示该事务消息(prepare状态的消息)已经处理过(提交或回滚),为未处理的事务进行事务回查提供查找依据。
  • 若为回滚事务,则直接执行删除prepare消息,没有恢复操作。

在这里,RocketMQ事务中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是UNKNOW状态,也可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。
RocketMQ将Op消息写入到RMQ_SYS_TRANS_OP_HALF_TOPIC主题中,不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作。示意图如下所示。
OP消息示意图

https://github.com/apache/rocketmq/tree/master/docs/cn

对于二阶段中没有确定状态的事务,不能就此放弃它们,后续要通过定时回查机制,来争取挽救它们。

为此,参考其他博主文章,做了一张事务消息发送的示意图,如下所示。
事务消息发送流程图

4.2 事务状态回查机制

当发送者的本地事务因为自身逻辑、中断或者其他的网络原因,导致无法立即响应的,RocketMQ一律当作UN_KNOW处理,即不提交也不删除prepare消息。对于这部分消息,RocketMQ事务还提供了一个补救方案:定时查询事务消息的本地事务状态。
RocketMQ broker端通过TransactionalMessageCheckService线程定时去检测RMQ_SYS_TRANS_HALF_TOPIC主题下的所有消息,对于没有操作过的消息(即没有在Op主题,表明没有被提交/回滚),进行回查消息的事务状态。默认情况下,检测频率为1分钟,回查最大检测次数是15次,如果超过最大检测次数还是无法获知消息的事务状态,则直接丢弃,即相当于回滚事务处理。
这部分代码比较长,就不再展示了,直接用个简易的流程图来表示吧,具体的代码在org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check。
事务回查流程图

五、总结

 RocketMQ事务消息基于两阶段提交和事务状态回查机制来实现,所谓的两阶段提交,即首先发送prepare消息,待事务提交或回滚时发送commit、rollback命令。再结合定时任务,RocketMQ使用专门的线程以特定的频率对RocketMQ服务器上的prepare信息进行处理。

六、参考文献

  1. RocketMQ事务消息学习及刨坑过程
  2. RocketMQ特性--事物消息【源码笔记】
  3. 《RocketMQ技术内幕》
posted @ 2021-01-17 23:45  弥有  阅读(177)  评论(0)    收藏  举报