RocketMQ学习笔记-事务性
RocketMQ学习笔记-事务性
一、前言
我们经常用支付宝来付钱/转账,这是日常生活的一件普通小事,但是我们思考支付宝扣除支付/转账的钱之后,如果系统挂掉怎么办,这时对方账户并没有增加相应的金额,数据就会出现不一致状况了。这种问题本质上可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。如果在单机系统(数据库实例也在一个系统)上,我们可以用本地事务轻松解决;但现在系统多为微服务架构,之间通过MQ传递消息,这时候就需要保证数据库事务和MQ事务的一致性。
我们以微服务架构的购物场景为例,参照一下RocketMQ官方的例子,用户A发起订单,支付100块钱操作完成后,能得到100积分,账户服务和会员服务是两个独立的微服务模块,有各自的数据库,可能出现以下情况:
- 如果先扣款,再发消息,可能钱刚扣完,宕机了,消息没发出去,结果积分没增加。
- 如果先发消息,然后再扣款,可能积分增加了,但钱没扣掉,白送人家100积分。
- 钱正常扣了,消息也发送成功了,但会员服务实例消费消息出现问题,结果积分没增加。
![转账场景]()
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事务消息通过异步确保方式,保证事务的最终一致性。设计流程上借鉴两阶段提交理论,流程图如下所示。

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

对于二阶段中没有确定状态的事务,不能就此放弃它们,后续要通过定时回查机制,来争取挽救它们。
为此,参考其他博主文章,做了一张事务消息发送的示意图,如下所示。

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信息进行处理。


浙公网安备 33010602011771号