RocketMQ提供了事務消息的功能,採用2PC(兩段式協議)+補償機制(事務回查)的分佈式事務功能,通過消息隊列 RocketMQ 版事務消息能達到分佈式事務的最終一致。
概覽
-
半事務消息:
暫不能投遞的消息,發送方已經成功地將消息發送到了消息隊列 RocketMQ 版服務端,但是服務端未收到生產者對該消息的二次確認,此時該消息被標記成“暫不能投遞”狀態,處於該種狀態下的消息即半事務消息。
-
消息回查:
由於網絡閃斷、生產者應用重啓等原因,導致某條事務消息的二次確認丟失,消息隊列 RocketMQ 版服務端通過掃描發現某條消息長期處於“半事務消息”時,需要主動向消息生產者詢問該消息的最終狀態(Commit 或是 Rollback),該詢問過程即消息回查。
交互流程
事務消息發送步驟如下:
- 發送方將半事務消息發送至消息隊列 RocketMQ 版服務端。
- 消息隊列 RocketMQ 版服務端將消息持久化成功之後,向發送方返回 Ack 確認消息已經發送成功,此時消息爲半事務消息。
- 發送方開始執行本地事務邏輯。
- 發送方根據本地事務執行結果向服務端提交二次確認(Commit 或是 Rollback),服務端收到 Commit 狀態則將半事務消息標記爲可投遞,訂閱方最終將收到該消息;服務端收到 Rollback 狀態則刪除半事務消息,訂閱方將不會接受該消息。
事務消息回查步驟如下:
- 在斷網或者是應用重啓的特殊情況下,上述步驟 4 提交的二次確認最終未到達服務端,經過固定時間後服務端將對該消息發起消息回查。
- 發送方收到消息回查後,需要檢查對應消息的本地事務執行的最終結果。
- 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍按照步驟 4 對半事務消息進行操作。
總體而言RocketMQ事務消息分爲兩條主線
-
發送流程:發送half message(半消息),執行本地事務,發送事務執行結果
-
定時任務回查流程:MQ定時任務掃描半消息,回查本地事務,發送事務執行結果
源碼分析
Producer是如何發送事務半消息的(prepare)
在本地應用發送事務消息的核心類是TransactionMQProducer
,該類通過繼承DefaultMQProducer來複用大部分發送消息相關的邏輯,這個類的代碼量非常少只有100來行,下面是這個類的sendMessageTransaction
方法
@Override
public TransactionSendResult sendMessageInTransaction(final Message msg,
final Object arg) throws MQClientException {
//判斷transactionListener是否存在
if (null == this.transactionListener) {
throw new MQClientException("TransactionListener is null", null);
}
//發送事務消息
return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
這裏的transactionListener
就是上面所說的消息回查的類,它提供了2個方法:
-
executeLocalTransaction
執行本地事務
-
checkLocalTransaction
回查本地事務
接着看DefaultMQProducer.sendMessageInTransaction()
方法:
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
//判斷檢查本地事務的listenner是否存在
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
//。。。省略
SendResult sendResult = null;
//msg設置參數TRAN_MSG,表示爲事務消息
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);
}
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;
}
try {
//執行endTransaction方法,如果半消息發送失敗或本地事務執行失敗告訴服務端是刪除半消息,半消息發送成功且本地事務執行成功則告訴服務端生效半消息
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
//...省略
return transactionSendResult;
}
該方法主要做了以下事情
-
給消息打上事務消息相關的tag,用於broker區分普通消息和事務消息
-
發送半消息(half message)
-
發送成功則由transactionListener執行本地事務
-
執行endTransaction方法,告訴 broker 執行 commit/rollback。
Broker端是如何處理事務消息的
Broker端通過SendMessageProcessor.processRequest()
方法接收處理 Producer 發送的消息 最後會調用到SendMessageProcessor.sendMessage()
,判斷消息類型,進行消息存儲。
//SendMessageProcessor.java
private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
final RemotingCommand request,
final SendMessageContext sendMessageContext,
final SendMessageRequestHeader requestHeader) throws RemotingCommandException {
//...省略
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);
}
接着看存儲半消息的代碼 prepareMessage(msgInner) :
//TransactionalMessageBridge.java
//存儲事務半消息
public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
return store.putMessage(parseHalfMessageInner(messageInner));
}
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
//備份消息的原主題名稱與原隊列ID
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和queueID是寫死固定的
msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
msgInner.setQueueId(0);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
return msgInner;
}
在這一步,備份消息的原主題名稱與原隊列ID,然後取消事務消息的消息標籤,重新設置消息的主題爲:RMQ_SYS_TRANS_HALF_TOPIC,隊列ID固定爲0。與其他普通消息區分開,然後完成消息持久化。
到這裏,Broker 就初步處理完了 Producer 發送的事務半消息。
執行本地事務
接着我們回到 上面 Producer 發送半消息的地方,往下繼續看。
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.executeLocalTransaction
方法執行本地事務。只有半消息發送成功後,纔會執行本地事務,如果半消息發送失敗,則設置回滾。
結束事務(commit/rollback)
本地事務執行後,則調用this.endTransaction()
方法,根據本地事務執行狀態,去提交事務或者回滾事務。
如果半消息發送失敗或本地事務執行失敗告訴服務端是刪除半消息,半消息發送成功且本地事務執行成功則告訴服務端生效半消息
public void endTransaction(
final SendResult sendResult,
final LocalTransactionState localTransactionState,
final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
final MessageId id;
if (sendResult.getOffsetMsgId() != null) {
id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
} else {
id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
}
String transactionId = sendResult.getTransactionId();
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;
}
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
requestHeader.setMsgId(sendResult.getMsgId());
String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
this.defaultMQProducer.getSendMsgTimeout());
}
半消息事務回查
兩段式協議發送與提交回滾消息,執行完本地事務消息的狀態爲UNKNOW
時,結束事務不做任何操作。通過事務狀態定時回查得到發送端的事務狀態是rollback或commit。
通過TransactionalMessageCheckService
線程定時去檢測RMQ_SYS_TRANS_HALF_TOPIC
主題中的消息,回查消息的事務狀態。
-
RMQ_SYS_TRANS_HALF_TOPIC
prepare消息的主題,事務消息首先先進入到該主題。
-
RMQ_SYS_TRANS_OP_HALF_TOPIC
當消息服務器收到事務消息的提交或回滾請求後,會將消息存儲在該主題下。
代碼入口:
public void run() {
log.info("Start transaction check service thread!");
//執行間隔
long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
while (!this.isStopped()) {
this.waitForRunning(checkInterval);
}
log.info("End transaction check service thread!");
}
@Override
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);
}
大致流程如下:
這裏重點說下判斷消息是否要回查的邏輯:
removeMap是個Map集合的鍵值對key是half隊列的消息offset,value是op隊列的消息offset,圖中看有兩對(100005,80002)、(100004,80003)
doneOpOffset是一個List集合,其中存儲的是op隊列的消息offset,圖中只有8004
check()循環查找half隊列中的消息時,100004已經在removeMap中了,跳過下面業務繼續循環下一個100005進行下一個邏輯,判斷其是否具有回查消息的條件isNeedCheck
Broker處理END_TRANSACTION
接下來我們來一起看看,當Producer或者回查定時任務提交/回滾事務的時候,Broker如何處理事務消息提交、回滾命令的。
//EndTransactionProcessor.java
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final EndTransactionRequestHeader requestHeader =
(EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);
LOGGER.debug("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;
}
//省略代碼,打印日誌
OperationResult result = new OperationResult();
//如果請求爲提交事務,進入事務消息提交處理流程
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
//根據commitLogOffset從commitlog文件中查找消息
result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
if (result.getResponseCode() == ResponseCode.SUCCESS) {
//字段檢查
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
//恢復事務消息的真實的主題、隊列,並設置事務ID
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);
//發送最終消息,存儲,被consumer消費
RemotingCommand sendResult = sendFinalMessage(msgInner);
if (sendResult.getCode() == ResponseCode.SUCCESS) {
//刪除預處理消息(prepare)
//其實是將消息存儲在主題爲:RMQ_SYS_TRANS_OP_HALF_TOPIC的主題中,代表這些消息已經被處理(提交或回滾)。
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return sendResult;
}
return res;
}
} //回滾處理
else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
//根據commitlogOffset查找消息
result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
if (result.getResponseCode() == ResponseCode.SUCCESS) {
//字段檢查
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
//刪除預處理消息(prepare)
//將消息存儲在RMQ_SYS_TRANS_OP_HALF_TOPIC中,代表該消息已被處理
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return res;
}
}
response.setCode(result.getResponseCode());
response.setRemark(result.getResponseRemark());
return response;
}
這裏的邏輯很清晰,其核心實現如下:
- 根據commitlogOffset找到消息
- 如果是提交動作,就恢復原消息的主題與隊列,再次存入commitlog文件進而轉到消息消費隊列,供消費者消費,然後將原預處理消息存入一個新的主題RMQ_SYS_TRANS_OP_HALF_TOPIC,代表該消息已被處理
- 回滾消息,則直接將原預處理消息存入一個新的主題RMQ_SYS_TRANS_OP_HALF_TOPIC,代表該消息已被處理
整體實現流程