分散式事務的一種解決思路(分散式事務一)
昨晚某技術群裡大家熱火的在討論分散式事務的問題,想起了自己前幾年由於技術太渣也犯過很多相關錯誤,現結合自己之前一次BUG案例由感而寫此文,希望對看到文章的同學們多少有些幫助(如果發現錯誤之處,歡迎交流)。
一個註冊業務,使用者註冊成功後,後臺呼叫另外一個服務同步完成開通資金賬戶,後來加了一個需求同時還要把註冊使用者資料同步到另一個業務系統中。
真實情況邏輯更復雜,現在簡化方便描述後相關虛擬碼如下:
@Transactional
public void register(){
//儲存使用者
saveUser();
//初始化賬戶
initAccount();
//推送使用者資訊
pushUser();
}
後面兩個方法可以理解成是其他業務系統的遠端服務;
程式碼上線後一段時間內倒也平穩沒出什麼問題(使用者數少),然而是bug早晚會復現的,最終在一個週末問題觸發了,接到領導通知,網站不能註冊,趕緊連上伺服器看日誌,發現日誌中大量超時及mysql死鎖異常,然後瞬間明白了問題原因,畢竟鍋是自己引起的。
pushUser()裡面是一個請求其他業務系統的HTTP介面,此處當時沒加connectTimeout超時限制,那天此業務系統介面異常,請求了60多秒還沒響應,這個整體方法上還有一個大事務,接著就造成了大量死鎖,再之後網站就不能註冊。
上面的這個示例可以說是分散式事務中常見的一類問題;一個業務的完成,要依賴於其他專案的多個遠端服務;但上面的那種寫法明顯問題很大,極易引發各種BUG,至少存在以下問題:
- 事務範圍過大
- 註冊業務嚴重耦合其他業務系統介面
資料不一致性問題
現在回想起來,當時果然是不知者無畏,啥程式碼都敢寫。
現在根據目前的認知水平針對上面問題,重新提供一個優化思路,使用MQ來解耦下面兩個遠端服務。
- 使用者資訊儲存成功後,插一條記錄到user_task_record表(user_id,account_flag,push_flag,create_time,update_time等欄位,兩個狀態欄位初始值預設為0);這兩個操作在同一個事務內;
- 扔一條訊息到MQ中;
- 消費者接受到廣播訊息後分別再處理initAccount、pushUser邏輯;相應消費者接收到訊息處理成功後,修改user_task_record表對應狀態欄位為1;
- 失敗重試機制,因為介面可能會有呼叫失敗的情況,新增一個5分鐘一次的定時任務,掃user_task_record表狀態為0的記錄,扔訊息到MQ中;
如果業務重要還可以加入監控預警,設定一個閥值,如果發現user_task_record表中create_time大於閥值並且狀態一直是0的記錄,可以給相關人員簡訊,郵件預警。
注意:由於有失敗重試機制,所以業務系統的相關介面必須是冪等的(冪等很重要),即我可以呼叫多次,不會產生重複資料。在本例中我們的消費者接受到訊息後,可以先從user_task表中查取下對應狀態是否為1,如果是1,說明業務邏輯已經執行成功,只用確認下訊息扔給下一個消費者處理;不等於1的就執行相應業務邏輯。
相關虛擬碼如下:
public void register(){
//儲存使用者
@Transactional
saveUser();
//生成一條訊息
producer.send(message);
}
簡易流程圖如下:
安利下mq,mq在實際開發中能幫我們很多忙:
在傳統的事務處理中,多個系統之間的互動耦合到一個事務中,響應時間長,影響系統可用性。引入分散式事務訊息,業務系統和訊息佇列之間,組成一個事務處理,能保證分散式系統之間資料的最終一致;下游業務系統(訂單交易、購物車、積分、其他)相互隔離,並行處理。
常見使用場景:
- 列表內容
- 列表內容
- 非同步解耦
- 削峰填谷
- 非同步通知
- 分散式事務處理
- ……
最後總結:
- 我們把2個同步遠端呼叫方法改成非同步了
- 事務範圍變小了
- 引入MQ,把業務解耦了,保證了分散式系統事務的最終一致性
- 專案更高可用了,不會因為其他業務系統引起專案宕機不可用