Spring Cloud分散式事務終極解決方案探討

vvsuperman發表於2018-01-17

一 前言

本方案已有講座講解:專題講座 歡迎圍觀

阿里2017雲棲大會《破解世界性技術難題!GTS讓分散式事務簡單高效》中,阿里聲稱提出了一種破解世界性難題之分散式事務的終極解決方案,無論是可靠性、還是處理速率都領先於市面上所有的技術。但令人遺憾的是一來專案未開源,二來還必須依賴阿里雲的分散式資料庫。畢竟,吃飯的傢伙可不能輕易示人嘛

雖然如此,但《世界難題...》一文中對事務還是歸納的還是蠻到位的:“一個看似簡單的功能,內部可能需要呼叫多個“服務”並操作多個資料庫或分片來實現,單一技術手段和解決方案已無法滿足這些複雜應用場景。因此,分散式系統架構中分散式事務是一個繞不過去的挑戰。

什麼是分散式事務?簡單的說,就是一次大操作由不同小操作組成,這些小操作分佈在不同伺服器上,分散式事務需要保證這些小操作要麼全部成功,要麼全部失敗。”

舉個例子:

你上Taobao買東西,需要先扣錢,然後商品庫存-1吧。但扣款和庫存分別屬於兩個服務,這兩個服務中間要經過網路、閘道器、主機等一系列中間層,萬一任何一個地方出了問題,比如網路抖動、突發異常等待,都會導致不一致,比如扣款成功了,但是庫存沒-1,就會出現超賣的現象,而這就是分散式事務需要解決的問題。

二 2階段提交(2PC, 3PC等)

2階段提交是分散式事務傳統解決方案,先進為止還廣泛存在。當一個事務跨越多個節點時,為了保持事務ACID特性,需要引入一個作為協調者來統一掌控所有節點(稱作參與者)的操作結果並最終指示這些節點是否要把操作結果進行真正的提交(比如將更新後的資料寫入磁碟等等)。因此,二階段提交的演算法思路可以概括為:參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。

以開會為例

甲乙丙丁四人要組織一個會議,需要確定會議時間,不妨設甲是協調者,乙丙丁是參與者。

投票階段

  1. 甲發郵件給乙丙丁,週二十點開會是否有時間;
  2. 甲回覆有時間;
  3. 乙回覆有時間;
  4. 丙遲遲不回覆,此時對於這個活動,甲乙丙均處於阻塞狀態,演算法無法繼續進行;
  5. 丙回覆有時間(或者沒有時間);

提交階段

  1. 協調者甲將收集到的結果反饋給乙丙丁(什麼時候反饋,以及反饋結果如何,在此例中取決與丙的時間與決定);
  2. 乙收到;
  3. 丙收到;
  4. 丁收到;

不僅要鎖住參與者的所有資源,而且要鎖住協調者資源,開銷大。一句話總結就是:2PC效率很低,對高併發很不友好。

引用《世界性難題...》一文原話 "國外具有幾十年歷史和技術沉澱的基於XA模型的商用分散式事務產品,在相同軟硬體條件下,開啟分散式事務後吞吐經常有數量級的下降。"

此外還有三階段提交

Spring Cloud分散式事務終極解決方案探討
大家有興趣的不妨研究下

三 柔性事務

所謂柔性事務是相對強制鎖表的剛性事務而言。流程入下:伺服器A的事務如果執行順利,那麼事務A就先行提交,如果事務B也執行順利,則事務B也提交,整個事務就算完成。但是如果事務B執行失敗,事務B本身回滾,這時事務A已經被提交,所以需要執行一個補償操作,將已經提交的事務A執行的操作作反操作,恢復到未執行前事務A的狀態。

缺點是業務侵入性太強,還要補償操作,缺乏普遍性,沒法大規模推廣。

四 訊息最終一致性解決方案之RocketMQ

目前基於訊息佇列的解決方案有阿里的RocketMQ,它實現了半訊息的解決方案,有點類似於Paxos演算法,具體流程如下

第一階段:上游應用執行業務併傳送MQ訊息

Spring Cloud分散式事務終極解決方案探討

  1. 上游應用傳送待確認訊息到可靠訊息系統
  2. 可靠訊息系統儲存待確認訊息並返回
  3. 上游應用執行本地業務
  4. 上游應用通知可靠訊息系統確認業務已執行併傳送訊息。

可靠訊息系統修改訊息狀態為傳送狀態並將訊息投遞到 MQ 中介軟體

第二階段:下游應用監聽 MQ 訊息並執行業務

下游應用監聽 MQ 訊息並執行業務,並且將訊息的消費結果通知可靠訊息服務。

Spring Cloud分散式事務終極解決方案探討

  1. 下游應用監聽 MQ 訊息元件並獲取訊息
  2. 下游應用根據 MQ 訊息體資訊處理本地業務
  3. 下游應用向 MQ
  4. 確認訊息被消費
  5. 下游應用通知可靠訊息系統訊息被成功消費,可靠訊息將該訊息狀態更改為已完成

RocketMQ貌似是一種先進的實現方案了,但問題是缺乏文件,無論是在Apache專案主頁,還是在阿里的頁面上,最多隻告訴你如何用,而原理性或者指導性的東西非常缺乏。

當然,如果你在阿里雲上專門購買了RocketMQ服務,想必是另當別論了。但如果你試圖在自己的服務環境中部署和使用,想必要歷經相當大的學習曲線。畢竟是人家吃飯的傢伙嘛

五 訊息最終一致性解決方案之RabbitMQ實現

RabbitMQ遵循了AMQP規範,用訊息確認機制來保證:只要訊息傳送,就能確保被消費者消費來做到了訊息最終一致性。而且開源,文件還異常豐富,貌似是實現分散式事務的良好載體

5.1 RabbitMQ訊息確認機制


rabbitmq的整個傳送過程如下

#####1. 生產者傳送訊息到訊息服務 #####2. 如果訊息落地持久化完成,則返回一個標誌給生產者。生產者拿到這個確認後,才能放心的說訊息終於成功發到訊息服務了。否則進入異常處理流程。 rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { if (!ack) { //try to resend msg } else { //delete msg in db } }); #####3. 訊息服務將訊息傳送給消費者 #####4. 消費者接受並處理訊息,如果處理成功則手動確認。當訊息服務拿到這個確認後,才放心的說終於消費完成了。否則重發,或者進入異常處理。 final Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8");

    System.out.println(" [x] Received '" + message + "'");
    try {
      doWork(message);
    } finally {
       //確認收到訊息
      channel.basicAck(envelope.getDeliveryTag(), false);
        }
      }
    };
複製程式碼

5.2 異常


我們來看看可能傳送異常的四種

1. 直接無法到達訊息服務

網路斷了,丟擲異常,業務直接回滾即可。如果出現connection closed錯誤,直接增加 connection數即可

    connectionFactory.setChannelCacheSize(100);
複製程式碼

2. 訊息已經到達伺服器,但返回的時候出現異常

rabbitmq提供了確認ack機制,可以用來確認訊息是否有返回。因此我們可以在傳送前在db中(記憶體或關係型資料庫)先存一下訊息,如果ack異常則進行重發

    /**confirmcallback用來確認訊息是否有送達訊息佇列*/     
    rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
    if (!ack) {
        //try to resend msg
    } else {
        //delete msg in db
    }
    });
     /**若訊息找不到對應的Exchange會先觸發returncallback */
    rabbitTemplate.setReturnCallback((message, replyCode, replyText, tmpExchange, tmpRoutingKey) -> {
        try {
            Thread.sleep(Constants.ONE_SECOND);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        log.info("send message failed: " + replyCode + " " + replyText);
        rabbitTemplate.send(message);
    });
複製程式碼

3. 訊息送達後,訊息服務自己掛了

如果設定了訊息持久化,那麼ack= true是在訊息持久化完成後,就是存到硬碟上之後再傳送的,確保訊息已經存在硬碟上,萬一訊息服務掛了,訊息服務恢復是能夠再重發訊息

4. 未送達消費者

訊息服務收到訊息後,訊息會處於"UNACK"的狀態,直到客戶端確認訊息

    channel.basicQos(1); // accept only one unack-ed message at a time (see below)
    final Consumer consumer = new DefaultConsumer(channel) {
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String message = new String(body, "UTF-8");

    System.out.println(" [x] Received '" + message + "'");
    try {
      doWork(message);
    } finally {
       //確認收到訊息
      channel.basicAck(envelope.getDeliveryTag(), false);
    }
      }
    };
    boolean autoAck = false;
    channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);
複製程式碼

5. 確認訊息丟失

訊息返回時假設確認訊息丟失了,那麼訊息服務會重發訊息。注意,如果你設定了autoAck= false,但又沒應答channel.baskAck也沒有應答channel.baskNack,那麼會導致非常嚴重的錯誤:訊息佇列會被堵塞住,所以,無論如何都必須應答

6. 消費者業務處理異常

訊息監聽接受訊息並處理,假設拋異常了,第一階段事物已經完成,如果要配置回滾則過於麻煩,即使做事務補償也可能事務補償失效的情況,所以這裡可以做一個重複執行,比如guavaretry,設定一個指數時間來迴圈執行,如果n次後依然失敗,發郵件、簡訊,用人肉來兜底。

六 總結

《世界性難題...》一文中對分散式事務的幾種實現方式進行了形象歸納

你每天上班,要經過一條10公里的只有兩條車道的馬路到達公司。這條路很堵,經常需要兩三個小時,上班時間沒有保證,這是2PC的問題-慢。

選擇一條很繞,長30公里但很少堵車的路,這是選b。上班時間有保證,但是必須早起,付出足夠的時間和汽油。這是柔性事務的問題,必須用具體業務來回滾,很難模組化

選擇一條有點繞,長20公里的山路,路不平,只有suv可以走,這是事務訊息最終一致性問題。引入了新的訊息中介軟體,需要額外的開發成本。但我司開發的CoolMQ已經對元件進行了封裝,只需要傳送,接受,就能滿足事務的要求。目前還有該方案的專題講座,大家可以根據自己的需要選用。

最後是GTSGTS修了一條擁有4條車道的高架橋,沒有繞路,還是10公里。不堵車,對事務來說是高效能;不繞路,對事務來說是簡單易用,對業務無侵入,不用為事務而重構;沒有車型限制,對事務來說是沒有功能限制,提供強一致事務。在沒有高架橋的時代,高架橋出現對交通來說就是一個顛覆性創新,很多以前看來無解的問題就迎刃而解了,同樣的,GTS希望通過創新改變資料一致性處理的行業現狀。但遺憾的是並未開源,而且需要結合阿里雲服務來使用。

相關文章