阿里面試官:RocketMQ與Kafka中如何實現事務?

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等

RocketMQ的事務是如何實現的?

首先我們來看 RocketMQ 的事務。我在之前的課程中,已經給大家講解過 RocketMQ 事務的大致流程,這裏我們再一起通過代碼,重溫一下這個流程。

public class CreateOrderService {
	@Inject
	private OrderDao orderDao;
	//注入訂單表的DAO
	@Inject
	private ExecutorService executorService;
	//注入一個ExecutorService
	private TransactionMQProducer producer;
	//初始化transactionListener和producer
	@Init
	public void init() throws MQClientException {
		TransactionListener transactionListener = createTransactionListener();
		producer = new TransactionMQProducer("myGroup");
		producer.setExecutorService(executorService);
		producer.setTransactionListener(transactionListener);
		producer.start();
	}
	//創建訂單服務的請求入口
	@PUT
	@RequestMapping(...)
	public Boolean createOrder(@RequestBody CreateOrderRequest request) {
		//根據創建訂單請求創建一條消息
		Message msg = createMessage(request);
		//發送事務消息
		SendResult sendResult = producer.sendMessageInTransaction(msg, request);
		//返回:事務是否成功
		return sendResult.getSendStatus() == SendStatus.SEND_OK;
	}
	private TransactionListener createTransactionListener() {
		return new TransactionListener() {
			@Override
			public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
				CreateOrderRequest request = (CreateOrderRequest ) arg;
				try {
					//執行本地事務創建訂單
					orderDao.createOrderInDB(request);
					//如果沒拋異常說明執行成功,提交事務消息
					return LocalTransactionState.COMMIT_MESSAGE;
				}
				catch (Throwable t) {
					//失敗則直接回滾事務消息
					return LocalTransactionState.ROLLBACK_MESSAGE;
				}
			}
			//反查本地事務
			@Override
			public LocalTransactionState checkLocalTransaction(MessageExt msg) {
				//從消息中獲得訂單ID
				String orderId = msg.getUserProperty("orderId");
				//去數據庫中查詢訂單號是否存在,如果存在則提交事務;
				//如果不存在,可能是本地事務失敗了,也可能是本地事務還在執行,所以返回UNKNOW//(PS:這裏RocketMQ有個拼寫錯誤:UNKNOW)
				return orderDao.isOrderIdExistsInDB(orderId)?
				LocalTransactionState.COMMIT_MESSAGE: LocalTransactionState.UNKNOW;
			}
		}
		;
	}
	//....
}

在這個流程中,我們提供一個創建訂單的服務,功能就是在數據庫中插入一條訂單記錄,併發送一條創建訂單的消息,要求寫數據庫和發消息這兩個操作在一個事務內執行,要麼都成功,要麼都失敗。在這段代碼中,我們首先在 init() 方法中初始化了 transactionListener和發生 RocketMQ 事務消息的變量 producer。真正提供創建訂單服務的方法是createOrder(),在這個方法裏面,我們根據請求的參數創建一條消息,然後調用RocketMQ producer 發送事務消息,並返回事務執行結果。

之後的 createTransactionListener() 方法是在 init() 方法中調用的,這裏面直接構造一個匿名類,來實現 RocketMQ 的 TransactionListener 接口,這個接口需要實現兩個方法:

  • executeLocalTransaction:執行本地事務,在這裏我們直接把訂單數據插入到數據庫中,並返回本地事務的執行結果。
  • checkLocalTransaction:反查本地事務,在這裏我們的處理是,在數據庫中查詢訂單號是否存在,如果存在則提交事務,如果不存在,可能是本地事務失敗了,也可能是本地事務還在執行,所以返回 UNKNOW。

這樣,就使用 RocketMQ 的事務消息功能實現了一個創建訂單的分佈式事務。接下來我們一起通過 RocketMQ 的源代碼來看一下,它的事務消息是如何實現的。

首先看一下在 producer 中,是如何來發送事務消息的:

public TransactionSendResult sendMessageInTransaction(final Message msg,final LocalTransactionExecuter locthrows 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.defaultM
	//調用發送普通消息的方法,發送這條半消息
	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
				if (null != transactionId && !"".equals(transactionId)) {
					msg.setTransactionId(transactionId);
				}
				//執行本地事務
				if (null != localTransactionExecuter) {
					localTransactionState = localTransactionExecuter.executeLocalTransac
					} else if (transactionListener != null) {
						log.debug("Used new transaction API");
						localTransactionState = transactionListener.executeLocalTransaction
						}
						if (null == localTransactionState) {
							localTransactionState = LocalTransactionState.UNKNOW;
						}
						if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
							log.info("executeLocalTransactionBranch return {}", localTransaction
							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;
			}
			//根據事務消息和本地事務的執行結果localTransactionState,決定提交或回滾事務消息
			//這裏給Broker發送提交或回滾事務的RPC請求。
			try {
				this.endTransaction(sendResult, localTransactionState, localException);
			}
			catch (Exception e) {
				log.warn("local transaction execute " + localTransactionState + ", but end broke
}
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;

這段代碼的實現邏輯是這樣的:首先給待發送消息添加了一個屬性PROPERTY_TRANSACTION_PREPARED,標明這是一個事務消息,也就是半消息,然後會像發送普通消息一樣去把這條消息發送到 Broker 上。如果發送成功了,就開始調用我們之前提供的接口 TransactionListener 的實現類中,執行本地事務的方法executeLocalTransaction() 來執行本地事務,在我們的例子中就是在數據庫中插入一條訂單記錄。

最後,根據半消息發送的結果和本地事務執行的結果,來決定提交或者回滾事務。在實現方法 endTransaction() 中,producer 就是給 Broker 發送了一個單向的 RPC 請求,告知Broker 完成事務的提交或者回滾。由於有事務反查的機制來兜底,這個 RPC 請求即使失敗或者丟失,也都不會影響事務最終的結果。最後構建事務消息的發送結果,並返回。

以上,就是 RocketMQ 在 Producer 這一端事務消息的實現,然後我們再看一下 Broker這一端,它是怎麼來處理事務消息和進行事務反查的。

Broker 在處理 Producer 發送消息的請求時,會根據消息中的屬性判斷一下,這條消息是普通消息還是半消息:

// ...
if (traFlag != null && Boolean.parseBoolean(traFlag)) {
	// ...
	putMessageResult = this.brokerController.getTransactionalMessageService().prepareMes
} else {
	putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
}
// ...

這段代碼在org.apache.rocketmq.broker.processor.SendMessageProcessor#sendMessage 方法中,然後我們跟進去看看真正處理半消息的業務邏輯,這段處理邏輯在類org.apache.rocketmq.broker.transaction.queue.TransactionalMessageBridge 中:

public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
	return store.putMessage(parseHalfMessageInner(messageInner));
}
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
	//記錄消息的主題和隊列,到新的屬性中
	MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getMessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
	String.valueOf(msgInner.getQueueId()));
	msgInner.setSysFlag(
	MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANS//替換消息的主題和隊列爲:RMQ_SYS_TRANS_HALF_TOPIC,0
	msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
	msgInner.setQueueId(0);
	msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProreturn msgInner;
}

我們可以看到,在這段代碼中,RocketMQ 並沒有把半消息保存到消息中客戶端指定的那個隊列中,而是記錄了原始的主題隊列後,把這個半消息保存在了一個特殊的內部主題RMQ_SYS_TRANS_HALF_TOPIC 中,使用的隊列號固定爲 0。這個主題和隊列對消費者是不可見的,所以裏面的消息永遠不會被消費。這樣,就保證了在事務提交成功之前,這個半消息對消費者來說是消費不到的。

然後我們再看一下,RocketMQ 是如何進行事務反查的:在 Broker 的TransactionalMessageCheckService 服務中啓動了一個定時器,定時從半消息隊列中讀出所有待反查的半消息,針對每個需要反查的半消息,Broker 會給對應的 Producer 發一個要求執行事務狀態反查的 RPC 請求,這部分的邏輯在方法org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener#sendCheckMessage 中,根據 RPC 返回響應中的反查結果,來決定這個半消息是需要提交還是回滾,或者後續繼續來反查。

最後,提交或者回滾事務實現的邏輯是差不多的,首先把半消息標記爲已處理,如果是提交事務,那就把半消息從半消息隊列中複製到這個消息真正的主題和隊列中去,如果要回滾事務,這一步什麼都不需要做,最後結束這個事務。這部分邏輯的實現在org.apache.rocketmq.broker.processor.EndTransactionProcessor 這個類中。

Kafka的事務和Exactly Once可以解決什麼問題?

接下來我們再說一下 Kafka 的事務。之前我們講事務的時候說過,Kafka 的事務解決的問題和 RocketMQ 是不太一樣的。RocketMQ 中的事務,它解決的問題是,確保執行本地事務和發消息這兩個操作,要麼都成功,要麼都失敗。並且,RocketMQ 增加了一個事務反查的機制,來儘量提高事務執行的成功率和數據一致性。

而 Kafka 中的事務,它解決的問題是,確保在一個事務中發送的多條消息,要麼都成功,要麼都失敗。注意,這裏面的多條消息不一定要在同一個主題和分區中,可以是發往多個主題和分區的消息。當然,你可以在 Kafka 的事務執行過程中,加入本地事務,來實現和RocketMQ 中事務類似的效果,但是 Kafka 是沒有事務反查機制的。

Kafka 的這種事務機制,單獨來使用的場景不多。更多的情況下被用來配合 Kafka 的冪等機制來實現 Kafka 的 Exactly Once 語義。我在之前的課程中也強調過,這裏面的 ExactlyOnce,和我們通常理解的消息隊列的服務水平中的 Exactly Once 是不一樣的。

我們通常理解消息隊列的服務水平中的 Exactly Once,它指的是,消息從生產者發送到Broker,然後消費者再從 Broker 拉取消息,然後進行消費。這個過程中,確保每一條消息恰好傳輸一次,不重不丟。我們之前說過,包括 Kafka 在內的幾個常見的開源消息隊列,都只能做到 At Least Once,也就是至少一次,保證消息不丟,但有可能會重複。做不到Exactly Once。

那 Kafka 中的 Exactly Once 又是解決的什麼問題呢?它解決的是,在流計算中,用 Kafka作爲數據源,並且將計算結果保存到 Kafka 這種場景下,數據從 Kafka 的某個主題中消費,在計算集羣中計算,再把計算結果保存在 Kafka 的其他主題中。這樣的過程中,保證每條消息都被恰好計算一次,確保計算結果正確。

舉個例子,比如,我們把所有訂單消息保存在一個 Kafka 的主題 Order 中,在 Flink 集羣中運行一個計算任務,統計每分鐘的訂單收入,然後把結果保存在另一個 Kafka 的主題Income 裏面。要保證計算結果準確,就要確保,無論是 Kafka 集羣還是 Flink 集羣中任何節點發生故障,每條消息都只能被計算一次,不能重複計算,否則計算結果就錯了。這裏面有一個很重要的限制條件,就是數據必須來自 Kafka 並且計算結果都必須保存到 Kafka中,纔可以享受到 Kafka 的 Excactly Once 機制。

可以看到,Kafka 的 Exactly Once 機制,是爲了解決在“讀數據 - 計算 - 保存結果”這樣的計算過程中數據不重不丟,而不是我們通常理解的使用消息隊列進行消息生產消費過程中的 Exactly Once。

Kafka的事務是如何實現的?

那 Kafka 的事務又是怎麼實現的呢?它的實現原理和 RocketMQ 的事務是差不多的,都是基於兩階段提交來實現的,但是實現的過程更加複雜。

首先說一下,參與 Kafka 事務的幾個角色,或者說是模塊。爲了解決分佈式事務問題,Kafka 引入了事務協調者這個角色,負責在服務端協調整個事務。這個協調者並不是一個獨立的進程,而是 Broker 進程的一部分,協調者和分區一樣通過選舉來保證自身的可用性。

和 RocketMQ 類似,Kafka 集羣中也有一個特殊的用於記錄事務日誌的主題,這個事務日誌主題的實現和普通的主題是一樣的,裏面記錄的數據就是類似於“開啓事務”“提交事務”這樣的事務日誌。日誌主題同樣也包含了很多的分區。在 Kafka 集羣中,可以存在多個協調者,每個協調者負責管理和使用事務日誌中的幾個分區。這樣設計,其實就是爲了能並行執行多個事務,提升性能。

下面說一下 Kafka 事務的實現流程。

首先,當我們開啓事務的時候,生產者會給協調者發一個請求來開啓事務,協調者在事務日誌中記錄下事務 ID。

然後,生產者在發送消息之前,還要給協調者發送請求,告知發送的消息屬於哪個主題和分區,這個信息也會被協調者記錄在事務日誌中。接下來,生產者就可以像發送普通消息一樣來發送事務消息,這裏和 RocketMQ 不同的是,RocketMQ 選擇把未提交的事務消息保存在特殊的隊列中,而 Kafka 在處理未提交的事務消息時,和普通消息是一樣的,直接發給 Broker,保存在這些消息對應的分區中,Kafka 會在客戶端的消費者中,暫時過濾未提交的事務消息。

消息發送完成後,生產者給協調者發送提交或回滾事務的請求,由協調者來開始兩階段提交,完成事務。第一階段,協調者把事務的狀態設置爲“預提交”,並寫入事務日誌。到這裏,實際上事務已經成功了,無論接下來發生什麼情況,事務最終都會被提交。

之後便開始第二階段,協調者在事務相關的所有分區中,都會寫一條“事務結束”的特殊消息,當 Kafka 的消費者,也就是客戶端,讀到這個事務結束的特殊消息之後,它就可以把之前暫時過濾的那些未提交的事務消息,放行給業務代碼進行消費了。最後,協調者記錄最後一條事務日誌,標識這個事務已經結束了。

我把整個事務的實現流程,繪製成一個簡單的時序圖放在這裏,便於你理解。

總結一下 Kafka 這個兩階段的流程,準備階段,生產者發消息給協調者開啓事務,然後消息發送到每個分區上。提交階段,生產者發消息給協調者提交事務,協調者給每個分區發一條“事務結束”的消息,完成分佈式事務提交。

總結

本文分別講解了 Kafka 和 RocketMQ 是如何來實現事務的。你可以看到,它們在實現事務過程中的一些共同的地方,它們都是基於兩階段提交來實現的事務,都利用了特殊的主題中的隊列和分區來記錄事務日誌。

不同之處在於對處於事務中的消息的處理方式,RocketMQ 是把這些消息暫存在一個特殊的隊列中,待事務提交後再移動到業務隊列中;而 Kafka 直接把消息放到對應的業務分區中,配合客戶端過濾來暫時屏蔽進行中的事務消息。

同時你需要了解,RocketMQ 和 Kafka 的事務,它們的適用場景是不一樣的,RocketMQ的事務適用於解決本地事務和發消息的數據一致性問題,而 Kafka 的事務則是用於實現它的 Exactly Once 機制,應用於實時計算的場景中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章