天天看點

分布式事務的一種解決思路

  昨晚某技術群裡大家熱火的在讨論分布式事務的問題,想起了自己前幾年由于技術太渣也犯過很多相關錯誤,現結合自己之前一次BUG案例由感而寫此文,希望對看到文章的同學們多少有些幫助(如果發現錯誤之處,歡迎交流)。

  一個注冊業務,使用者注冊成功後,背景調用另外一個服務同步完成開通資金賬戶,後來加了一個需求同時還要把注冊使用者資料同步到另一個業務系統中。

  真實情況邏輯更複雜,現在簡化友善描述後相關僞代碼如下:

@Transactional
public void register(){
    //儲存使用者
    saveUser();
    //初始化賬戶
    initAccount();
    //推送使用者資訊
    pushUser();
}      

  後面兩個方法可以了解成是其他業務系統的遠端服務;

  代碼上線後一段時間内倒也平穩沒出什麼問題(使用者數少),然而是bug早晚會複現的,最終在一個周末問題觸發了,接到上司通知,網站不能注冊,趕緊連上伺服器看日志,發現日志中大量逾時及mysql死鎖異常,然後瞬間明白了問題原因,畢竟鍋是自己引起的。

  pushUser()裡面是一個請求其他業務系統的HTTP接口,此處當時沒加connectTimeout逾時限制,那天此業務系統接口異常,請求了60多秒還沒響應,這個整體方法上還有一個大事務,接着就造成了大量死鎖,再之後網站就不能注冊。

  上面的這個示例可以說是分布式事務中常見的一類問題;一個業務的完成,要依賴于其他項目的多個遠端服務;但上面的那種寫法明顯問題很大,極易引發各種BUG,至少存在以下問題:

  1. 事務範圍過大
  2. 注冊業務嚴重耦合其他業務系統接口
  3. 資料不一緻性問題

 現在回想起來,當時果然是不知者無畏,啥代碼都敢寫。

 現在根據目前的認知水準針對上面問題,重新提供一個優化思路,使用MQ來解耦下面兩個遠端服務。

  1. 使用者資訊儲存成功後,插一條記錄到user_task_record表(user_id,account_flag,push_flag,create_time,update_time等字段,兩個狀态字段初始值預設為0);這兩個操作在同一個事務内;
  2. 扔一條消息到MQ中;
  3. 消費者接受到廣播消息後分别再處理initAccount、pushUser邏輯;相應消費者接收到消息處理成功後,修改user_task_record表對應狀态字段為1;
  4. 失敗重試機制,因為接口可能會有調用失敗的情況,新增一個5分鐘一次的定時任務,掃user_task_record表狀态為0的記錄,扔消息到MQ中;
  5. 如果業務重要還可以加入監控預警,設定一個閥值,如果發現user_task_record表中create_time大于閥值并且狀态一直是0的記錄,可以給相關人員短信,郵件預警。

 注意:由于有失敗重試機制,是以業務系統的相關接口必須是幂等的(幂等很重要),即我可以調用多次,不會産生重複資料。在本例中我們的消費者接受到消息後,可以先從user_task表中查取下對應狀态是否為1,如果是1,說明業務邏輯已經執行成功,隻用确認下消息扔給下一個消費者處理;不等于1的就執行相應業務邏輯。

相關僞代碼如下:

public void register(){
    //儲存使用者
    @Transactional
    saveUser();
        //生成一條消息
    producer.send(message);
}      

簡易流程圖如下:

分布式事務的一種解決思路

安利下mq,mq在實際開發中能幫我們很多忙:

  在傳統的事務進行中,多個系統之間的互動耦合到一個事務中,響應時間長,影響系統可用性。引入分布式事務消息,業務系統和消息隊列之間,組成一個事務處理,能保證分布式系統之間資料的最終一緻;下遊業務系統(訂單交易、購物車、積分、其他)互相隔離,并行處理。

常見使用場景:

  1. 異步解耦
  2. 削峰填谷
  3. 異步通知
  4. 分布式事務處理
  5. ......

最後總結:

  1. 我們把2個同步遠端調用方法改成異步了
  2. 事務範圍變小了
  3. 引入MQ,把業務解耦了,保證了分布式系統事務的最終一緻性
  4. 項目更高可用了,不會因為其他業務系統引起項目當機不可用