學習思路
- 事務消息分析
- 實際中怎麼使用
- 根據事務消息實現最終一致性(分佈式事務實現)
一、事務消息分析
先看一張事務消息的時序圖
解釋
- 第一步先發送消息到MQServer端,這個時候Server端消息並未到消費隊列
- 發送成功後執行本地事務
- 執行完本地事務後,將執行結果(成功/失敗)推送到MQServer
- 如果推送失敗,或者MQServer長時間未接受到本地事務執行結果,輪循(默認1分鐘)Check本地事務
- 如果MQServer端接收到本地事務成功:將消息放入消費隊列,如果失敗:作廢該消息
我們來看一下源碼
/**
* 發送事務消息
* @param msg
* @param localTransactionExecuter 廢棄,不建議傳入值,通過監聽重寫
* @param arg
* @return 返回發送結果
* @throws MQClientException
*/
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
//獲取執行本地事務,check本地事務的監聽
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);
}
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 {
//執行完本地事務後發送MQServer執行狀態
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
//包裝事務消息發送結果返回
TransactionSendResult transactionSendResult = new TransactionSendResult();
transactionSendResult.setSendStatus(sendResult.getSendStatus());
transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
transactionSendResult.setMsgId(sendResult.getMsgId());
transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
transactionSendResult.setTransactionId(sendResult.getTransactionId());
transactionSendResult.setLocalTransactionState(localTransactionState);
return transactionSendResult;
}
解釋:
- getCheckListener():這個主要是重寫了本地事務執行和Check本地事務執行狀態兩個方法
- 發送消息到Server端
- 如果發送成功執行第一步重寫的:本地事務執行方法
- endTransaction():執行完本地事務後將執行狀態發送MQServer,完成事務消息發送
- 包裝消息發送結果返回
我們來看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();
//獲取Server端Broker地址
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:
//如果本地事務執行成功設置Header成功
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;
}
//設置消息生產者分組,如果將本地事務結果推送到Server端失敗,Server端Check時用
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());
}
解釋:
- 根據本地事務執行結果設置MQ需要的枚舉
- 設置Header
- 單次發送MQServer本地事務執行結果
我們再看TransactionListener
是org.apache.rocketmq.client.producer.TransactionMQProducer的一個屬性我們在發送事務消息的時候要實現TransactionListener裏的方法
public interface TransactionListener {
/**
* When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
* 發送事務消息,消息發送Server端成功時,執行本方法(本地事務)
*
* @param msg Half(prepare) message
* @param arg Custom business parameter
* @return Transaction state
*/
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
/**
* When no response to prepare(half) message. broker will send check message to check the transaction status, and this
* method will be invoked to get local transaction status.
* 當執行完本地事務後,Server端遲遲未收到最後通知,server端會調用次方法檢查本地事務狀態
*
* @param msg Check message
* @return Transaction state
*/
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}
看註釋、、
二、實際中怎麼使用
可參考源碼中的例子
發送:org.apache.rocketmq.example.transaction.TransactionProducer
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
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);
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();
}
TransactionListenerImpl就是我們上面講解的TransactionListener的實現
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
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;
}
}
這裏推薦一種實際開發中更簡潔的方法-Apache推出的,可放心使用rocketmq-sping
點進去你會發現是如此的簡單
三、根據事務消息實現最終一致性(分佈式事務實現)
分佈式事務概念可查看歷史文章:從本地事務到分佈式事務到微服務下事務
- 首先我們在發送消息的時候確保了:本地事務和發送消息要麼全部成功要不全部失敗
- 消息消費時,如果消費失敗服務異常了,會觸發階梯時間重試(默認16次)
- 如果最大重試此時後還是失敗,會進入死信隊列,需要人工介入觸發消,但消息本地持久化時間默認爲72小時
- 這基本可保障了99%的最終一致性,當然可根據實際情況自己實現,如:如果最大次數通知失敗,記錄數據庫,人工處理等、、、
公衆號主要記錄各種源碼、面試題、微服務技術棧,幫忙關注一波,非常感謝