分散式事務(5)---最終一致性方案之可靠訊息

白露非霜發表於2021-12-14

 

分散式事務(1)-理論基礎

分散式事務(2)---強一致性分散式事務解決方案

分散式事務(3)---強一致性分散式事務Atomikos實戰

分散式事務(4)---最終一致性方案之TCC

 

可靠訊息最終一致性是解決分散式事務中一種典型的柔性方案。通常有兩種實現方式,一種是基於本地訊息表,一種是基於RocketMQ的事務訊息。需要注意傳送訊息的一致性和訊息的可靠性。

基本原理:

事務發起方執行本地事務成功後發出一條訊息,事務參與方也就是訊息的消費者,接收到訊息並執行成功本地事務。這樣來達到資料的最終一致性。

需要注意發起方一定能夠將訊息傳送出去,參與方一定能成功接收到訊息。這樣來確保訊息的可靠性。否則同樣會出現分散式事務問題。

本地訊息表

為了防止在使用訊息一致性方案時,出現訊息丟失,可以使用本地訊息表來保證訊息的傳送。通過本地事務將業務資料和訊息寫入本地資料庫,這一步操作是本地事務可以保證訊息表必然會寫入資料。然後通過定時任務讀物本地訊息表中的資料,將訊息傳送給訊息中介軟體。如果傳送失敗,進行重試,因此還涉及到冪等操作。消費方接收到訊息之後,執行業務(本地事務)成功,則完成分散式事務,若失敗則進行重試。如果多次任然失敗,則通知事務發起方進行事務回滾。

 

方案存在如下缺點:

1.訊息表耦合在業務庫中,需要額外的處理髮送訊息的操作,不利於訊息的擴充套件,同事如果訊息表堆積了大量訊息資料,會對業務操作產生一定的效能影響。

2.訊息傳送失敗需要重試,需要保證操作的相關操作的冪等

3.多次重試依然失敗需要人工干預

4.訊息服務與業務耦合,不利於訊息服務的擴充套件。

 

RocketMQ事務訊息

RocketMQ在4.3版本後引入了完整的事務訊息機制,其內部實現了本地訊息表的邏輯,使用其事務訊息極大的減輕了開發的工作量。

在RocketMQ中,producer和broker具有雙向通訊能力,使得broker自然的具備了事務協調者的能力。

RocketMQ事務訊息分散式事務解決方案原理圖:

 

 

 

 

 

 

 roketMQ事務訊息案例,官方複製貼上:

事務訊息共有三種狀態,提交狀態、回滾狀態、中間狀態:

  • TransactionStatus.CommitTransaction: 提交事務,它允許消費者消費此訊息。
  • TransactionStatus.RollbackTransaction: 回滾事務,它代表該訊息將被刪除,不允許被消費。
  • TransactionStatus.Unknown: 中間狀態,它代表需要檢查訊息佇列來確定狀態。

1、建立事務性生產者 使用 TransactionMQProducer類建立生產者,並指定唯一的 ProducerGroup,就可以設定自定義執行緒池來處理這些檢查請求。執行本地事務後、需要根據執行結果對訊息佇列進行回覆。 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class 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(); } } 2、實現事務的監聽介面 當傳送半訊息成功時,我們使用 executeLocalTransaction 方法來執行本地事務。它返回前一節中提到的三個事務狀態之一。checkLocalTransaction 方法用於檢查本地事務狀態,並回應訊息佇列的檢查請求。它也是返回前一節中提到的三個事務狀態之一。 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; } } return LocalTransactionState.COMMIT_MESSAGE; } } 3. 事務訊息使用上的限制 1.事務訊息不支援延時訊息和批量訊息。 2.為了避免單個訊息被檢查太多次而導致半佇列訊息累積,我們預設將單個訊息的檢查次數限制為 15 次,但是使用者可以通過 Broker 配置檔案的 transactionCheckMax引數來修改此限制。如果已經檢查某條訊息超過 N 次的話( N = transactionCheckMax ) 則 Broker 將丟棄此訊息,並在預設情況下同時列印錯誤日誌。使用者可以通過重寫 AbstractTransactionalMessageCheckListener 類來修改這個行為。 3.事務訊息將在 Broker 配置檔案中的引數 transactionTimeout 這樣的特定時間長度之後被檢查。當傳送事務訊息時,使用者還可以通過設定使用者屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該引數優先於 transactionTimeout 引數。 4.事務性訊息可能不止一次被檢查或消費。 5.提交給使用者的目標主題訊息可能會失敗,目前這依日誌的記錄而定。它的高可用性通過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務訊息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。 6.事務訊息的生產者 ID 不能與其他型別訊息的生產者 ID 共享。與其他型別的訊息不同,事務訊息允許反向查詢、MQ伺服器能通過它們的生產者 ID 查詢到消費者。

  

訊息傳送的一致性

訊息傳送的一致性指的事務發起方執行本地事務成功則一定能把其產生的訊息傳送出去。這裡涉及到訊息傳送與確認機制,訊息傳送的不可靠性,如何保證訊息傳送的一致性。

訊息傳送與確認機制:

常規中間的訊息傳送與確認機制如下:

1.生產者執行本地事務,然後將訊息傳送到MQ,這裡可以是同步或者非同步

2.MQ接收到訊息後,將訊息資料持久化到磁碟。這個MQ都會提供相應的配置

3.MQ向生產者返回傳送結果(訊息狀態或者異常)

4.消費者監聽消費訊息

5.消費者執行本地事務

6.消費者向訊息MQ確認消費訊息

這種流程一般來說無法保證訊息傳送的一致性。

訊息傳送如何不一致:

1.先運算元據庫,再傳送訊息。資料庫寫入了,但訊息可能沒有傳送出去,事務參與方就沒有訊息可消費

    public void tx() {
        //1.執行業務
        //2.傳送訊息
    }

2.先發訊息,在操作庫。訊息發出去,但是本地事務執行失敗,參與方可以執行業務,但是發起方沒有執行業務

    public void tx() {
        //1.傳送訊息
        //2.執行業務
    }

3.同一事務中,先發訊息,再操作庫。和第二點一樣,事務回滾無法控制訊息的回滾

    @Transactional
    public void tx() {
        //1.傳送訊息
        //2.執行業務
    }

4.同一事務中,先操作庫,再傳送訊息。這種看似正常,資料儲存成功,訊息傳送失敗,事務會回滾。但是如果事務執行成功,訊息傳送成功,由於網路原因,導致傳送訊息相應超時,丟擲異常回滾了事務,這個時候訊息可能已經被事務參與方消費了,並執行了業務。所以還是需要傳送確認機制。流程參考上面RocketMQ事務訊息流程圖

    @Transactional
    public void tx() {
        //1.執行業務
        //2.傳送訊息
    }

 

訊息接收的一致性:

訊息接收與確認

 訊息接收的一致性在一定程度上需要滿足訊息的接收與確認機制:

1.MQ向消費方投遞訊息

2.消費方收到訊息,執行本地事務,執行成功/失敗,將結果傳送給MQ

3.中介軟體處理消費者發來的結果,成功則清除訊息記錄,失敗則根據不同的情況處理,比如rabbitMQ,可以設定重回佇列

4.MQ投遞訊息失敗會進行重試,多次投遞失敗,將訊息轉入死信佇列,以便後面人工處理

5.消費方執行完業務,如果如法將結果傳送給MQ,同樣應該引入重試機制,比如另起執行緒,掃表資料狀態,將結果傳送給MQ

需要注意:1.訊息接收介面需要保證冪等;2.涉及到重試,最好設定重試次數,以免進入死迴圈。

 

訊息接收不一致:

1.接收訊息的介面沒有冪等,如果訊息重複投遞則會導致資料不一致。

2.消費者可能無法接收訊息,此時MQ並沒有重試投遞,導致事務參與方業務沒有執行,引起資料不一致

3.消費者執行完本地業務後,無法將結果反饋給MQ,MQ無法正確的處理訊息,進行了重試,消費介面又沒有冪等導致資料一不一致

如何保證訊息接收的一致性:

1.限制MQ訊息投遞重試的最大次數

2.訊息接收介面保證冪等

3.事務參與方與MQ之間需要確認機制

4.失敗的訊息轉入私信佇列,後續人工干預處理

 

相關文章