目录
1:实现思想
RocketMQ事务消息的实现原理是基于两阶段提交(可以去了解一下XA)和定时事务状态回查来决定消息最终是提交还是回滚。一般地,应用程序在事务内完成相应的DB后,需要同步来调用mq相关的接口来发送消息,发送状态为prepare的消息(笔者称之为预消息),消息发送成功后,Rocketmq服务器会回调RocketMQ消息发送者的事件监听程序,记录消息的本地事务状态(相当于打一个标),该标与本地业务操作同属一个事务,确保消息发送与本地事务的原子性。
mq收到prepare的消息,会先备份消息的原主题和原消息消费队列(消息的载体),然后将消息存储为主题为 RMQ_SYS_TRANS_HALF_TOPIC(笔者称之为半主题)的消息消费队列中。
RocketMQ消息服务器开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC消息队列的中消息,向消息发送端发起事务状态回查,应用程序根据保存的事务状态来回馈消息服务器事务的状态(commit,rollback,unkown),如果是提交或者是回滚,则服务提交或者回滚消息,如果是未知的消息,等待下一次回查,(为什么会存在未知的消息,一次轮询过去,而这个消息刚刚出来,还没有被消费),RocketMQ允许一条消息的回查间隔(轮询周期)与回查次数(超过多少次回滚),如果达到了n(最大回查次数)次,将会回滚消息
2:事务消息发送流程
1:入口很重要:发送事务消息的开始和普通的消息有点不同,事务消息的producer是TransactionMQProducer,如果看过笔者之前写过的话,应该知道是DefaultMQProducer,其实TransactionMQProducer是DefaultMQProducer的增强,在TransactionMQProducer的内部有一个transactionlistener,主要实现了本地事务状态执行,和本地事务事务回查两个接口
2:事务消息发送依赖的是TransactionMQProducer
@Override
//通俗易懂,在事务中发送消息
public TransactionSendResult sendMessageInTransaction(final Message msg,
final Object arg) throws MQClientException {
//这里必须要依赖事务监听器,要是这个都没有话,相当于不可能存在事务状态回查和本地事务执行
if (null == this.transactionListener) {
throw new MQClientException("TransactionListener is null", null);
}
//设置系统的半topic
msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
//最后还是调用的默认的mqProducer
return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
3:设置消息的主题,和生产者组
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
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);
}
4:执行本地事务
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
case SEND_OK: {
try {
if (sendResult.getTransactionId() != null) {
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
//从消息中获取事务id,为后续事务回查提供依据
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;
}
}
5:check消息,发送消息
//是否设置了事务消息
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
//如果是事务消息的话,会给消息一个 PRE 的标志,后续会添加一个事务消息的专用处理器
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
Map<String, String> oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties());
//看看是不是事务消息
String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (traFlag != null && Boolean.parseBoolean(traFlag)) {
if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark(
"the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
+ "] sending transaction message is forbidden");
return response;
}
putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
} else {
putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
}
return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);
6:由于上述的消息的主题已经被改变成一个系统的主题,并且放入到对应的消息队列中,那么在消费的时候肯定是一个专门的线程去消费这个消息。
3:消息提交,回滚
1:在事务消息结束(没有真正的结束,只是一阶段的结束)事务消息的提交或者是回滚均由sendResult这个字段标识来确定,对应的处理器为org.apache.rocketmq.broker.processor.EndTransactionProcessor#processRequest,因为涉及到传输效率,在RocketMq中大量的使用Netty来做为io框架,所以涉及到数据的加解码(就是各种handler啦)
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final EndTransactionRequestHeader requestHeader =
//加解码
(EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);
LOGGER.info("Transaction request:{}", requestHeader);
if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {
response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
LOGGER.warn("Message store is slave mode, so end transaction is forbidden. ");
return response;
}
2:确定是否提交或者是回滚事务
perationResult result = new OperationResult();
//提交事务
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
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());
//获得事务消息的偏移量,然后要将此消息放入到commitlog中进行文件的存储,最终是要存盘的
msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
//存储时间戳,一般用于消息的时间查找
msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
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;
}
}
response.setCode(result.getResponseCode());
response.setRemark(result.getResponseRemark());
return response;
4:回查事务状态
1:可以很绝对的认为,在RocketMQ中的事务消息只会存在两种状态(分别是 commit,rollback,)至于unkown的话只能做为一个中间的状态,也就是说,随着时间的推移,这种状态是一定会不存在的,要是真的不行,那就搬出来微积分--极限思想,世间万物最终归于平静,其实大数定律也是如此(扯得有点哲学的味道了,偏题了,偏题了)。
2:定时任务去扫描HALF_TOPIC,回查消息的事务状态
protected void onWaitEnd() {
//超时时间
long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
//最大的配置检测时间
int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
//当前时间
long begin = System.currentTimeMillis();
log.info("Begin to check prepare message, begin time:{}", begin);
this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
}
3:拉取HALF_TOPIC主题下的所有消息队列,然后依次处理
public void check(long transactionTimeout, int transactionCheckMax,
AbstractTransactionalMessageCheckListener listener) {
try {
String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC;
Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
if (msgQueues == null || msgQueues.size() == 0) {
log.warn("The queue of topic is empty :" + topic);
return;
}
log.debug("Check topic={}, queues={}", topic, msgQueues);
for (MessageQueue messageQueue : msgQueues) {
long startTime = System.currentTimeMillis();
MessageQueue opQueue = getOpQueue(messageQueue);
long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
if (halfOffset < 0 || opOffset < 0) {
log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
halfOffset, opOffset);
continue;
}
4:事务回查实现
@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;
5:总结
在RocketMQ中事务消息底层包装的是普通消息,在发送的时候更换其TOPIC,和对应的消息队列,然后有一个线程来消费这个消息队列上面的消息,分为两阶段提交,第一阶段是本地执行,第二阶段才是真正的提交,不过第二阶段的提交也是由条件的,而是通过事务状态回查的方式的来是实现的,最后会更新事务消息的ID,已达到将原来的消息更换成普通的消息进行发送,接着就会存入磁盘中持久化。