大型網站的分散式事務
我們都熟悉mysql事務,它保證要麼全做,要麼全不做。但是對於一個大型系統來說,mysql一般會分庫分表,多物理機部署,同時業務邏輯更加複雜漫長,同時需要呼叫很多服務(SOA)。
因此可以想象,在一個處理邏輯中,任何一箇中間環節出現異常中斷(請求超時,程序自殺等等不可控因素)都是很麻煩的。麻煩在於我們要知道上次中斷在哪裡以便重試的時候可以跳過之前的環節,這對編寫函式流程的方式方法論就有一定的要求了。
我們知道,任何一個可靠的系統都應該符合”可重入”,”冪等性”,因此我們也相信我們依賴的其他服務也是遵循這些守則的,因此我們重試或者併發的訪問其他服務都會得到正確且一致的應答。有了這個前提,我們才能討論如何解決”分散式事務”問題。另外,這裡提到的”分散式事務”是”柔性事務”,只能保證最終一致性,因為任何中間環節中斷是無法避免的,並且通常不考慮中途回滾,需要事務完整做完再進入其他回滾流程。
分散式事務案例
繼續以”付款環節”為例,當我們收到付款後,希望給使用者生成消費碼,然後給使用者傳送一條簡訊告知消費碼,最終返回”接受付款”。為了簡化,我們不考慮”拒絕付款”的場景,整個流程簡化如下:繼續以”付款環節”為例,當我們收到付款後,希望給使用者生成消費碼,然後給使用者傳送一條簡訊告知消費碼,最終返回”接受付款”。為了簡化,我們不考慮”拒絕付款”的場景,整個流程簡化如下:
-
微信支付系統呼叫我們的介面,告知我們收到了order訂單付款。
-
從mysql讀取這個order,並且假設order的status總是”待付款”。
-
請求券碼服務,為這個order分配1個消費碼。
-
更新mysql將消費碼存到order中。
-
請求簡訊服務,為這個order的使用者傳送這個消費碼。
-
更新這個訂單為”已付款”。
-
返回微信支付”接受付款”。
整個流程比較明確,我們完全可以順序的把程式碼編出來,在各種環境都非常穩定的情況下,可以正常執行。但是程式複雜的永遠是異常,下面我們分別考慮每個步驟失敗會帶來什麼問題。
第三步:券碼服務超時,但其服務內部實際上已經生成了一個券碼,按照部落格之前的介紹,我們只能退出程式,稍後重試。問題是重試的時候,我們再次請求券碼服務,會不會又多生成了1個券碼呢?其實很好解決,券碼服務的介面設計滿足冪等性即可,例如:createCoupon(orderId),我們每次呼叫傳入訂單號,券碼服務在mysql中維護orderId唯一索引,從而保證一個order只能生成一次券碼。問題得到解決。
第四部:更新mysql超時,分兩種情況,雖然超時但已經寫入到mysql和沒有寫入。在重試的時候,我們可能會想到,可以首先判斷order中是否已經填寫了消費碼,如果有就不需要執行第三步和第四步了,否則依舊從第三步開始執行。
第五步:簡訊服務超時,其實和第三步的場景一樣,簡訊服務可以保證冪等性,從而不會因為異常導致重試的時候給使用者多發1次簡訊。
第六步:更新mysql超時,也分實際寫入和沒有寫入mysql。在重試的時候,如果order的status是”已付款”,那麼直接就應該跳轉步驟7),否則仍舊應該從第五步執行。
_ueditor_page_break_tag_
迴歸問題本質,降低問題複雜度從2個方向入手。首先這一段邏輯是寫在一個函式流程裡的,先做1後做2…最後做7,我們仍舊想保持這麼簡單的程式碼。其次,我們期望的效果是即便做到6失敗了,在重試的時候可以直接從6開始重做,而不是從1做到7。
根據上述思路,我們除了保持訂單的status不變,新增一個sub_status欄位用於記錄”分散式事務”的中間狀態,如果sub_status=5代表步驟5執行成功,且步驟6尚未執行或者上一次執行失敗。這樣,每完成一個步驟,我們將sub_status記錄到mysql中,以便下次重試可以直接從中斷的sub_status位置繼續執行邏輯處理。
而整個程式碼將被簡化成如下虛擬碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
上述都是一些基本的理念,僅具有指導意義。實際問題還是需要實際分析,記住這些思路,是為了在解決具體問題時能幫助梳理思緒,簡化問題。
歡迎關注博主公眾號,為您推薦更多好文,手機掃下方二維碼