Go 工程師體系課 013【學習筆記】

目錄

訂單事務

  • 先扣庫存、後扣庫存都會對庫存和訂單產生影響,所以要使用分散式事務(Distributed Transaction)
  • 業務(下單不支付)業務問題
  • 支付成功再扣減(下單了,支付時沒庫存了)
  • 訂單扣減,不支付(訂單超時歸還)【常用方式】

事務與分散式事務

1. 什麼是事務?

事務(Transaction)是資料庫管理系統中的一個重要概念,它是一組資料庫操作的集合,這些操作要嘛全部成功執行,要嘛全部失敗回滾。

1.1 事務的 ACID 特性

  • 原子性(Atomicity):事務中的所有操作要嘛全部成功,要嘛全部失敗,不存在部分成功的情況
  • 一致性(Consistency):事務執行前後,資料庫從一個一致狀態轉換到另一個一致狀態
  • 隔離性(Isolation):並行執行的事務之間相互隔離,一個事務的執行不應影響其他事務
  • 持久性(Durability):事務一旦提交,其結果就永久保存在資料庫中

1.2 事務的隔離級別

  1. 讀未提交(Read Uncommitted):最低級別,可能讀到髒資料
  2. 讀已提交(Read Committed):只能讀到已提交的資料
  3. 可重複讀(Repeatable Read):同一事務中多次讀取結果一致
  4. 串行化(Serializable):最高級別,完全串行執行

2. 什麼是分散式事務?

分散式事務(Distributed Transaction)是指涉及多個資料庫或服務的事務操作,需要保證跨多個節點的資料一致性。

2.1 分散式事務的挑戰

  • 網路分割(Network Partition):網路故障導致節點間通訊中斷
  • 節點故障(Node Failure):某個節點當機或重啟
  • 時鐘不同步(Clock Skew):各節點時間不一致
  • 資料一致性(Data Consistency):如何保證跨節點的資料一致性

2.2 CAP 理論

  • 一致性(Consistency):所有節點在同一時間看到相同的資料(更新返回客戶端後)
  • 可用性(Availability):系統持續可用,不會出現操作失敗
  • 分割容錯性(Partition Tolerance):系統能夠容忍網路分割故障

CAP 定理:在分散式系統中,最多只能同時滿足 CAP 中的兩個特性。

2.3 BASE 理論(與 CAP 的工程化取捨)

  • Basically Available(基本可用):在發生故障時,允許系統降級提供有限功能(如回應變慢、部分功能不可用)
  • Soft state(軟狀態):系統狀態允許在一段時間內存在中間態(未強一致)
  • Eventual consistency(最終一致):經過一段時間(或重試/補償)後,資料達到一致

工程實踐中:多數網際網路業務選擇 AP → 以 BASE 理論為指導,犧牲強一致性,換取高可用與可擴展性,透過「補償、重試、去重、對帳」實現最終一致性。

3. 分散式事務解決方案

3.1 兩階段提交(2PC)

原理

  1. 準備階段:協調者詢問所有參與者是否可以提交
  2. 提交階段:根據參與者回應決定提交或回滾

優點:強一致性
缺點:效能差、單點故障、阻塞問題

流程細化(示意)

  1. 協調者向參與者發送 prepare 請求,各參與者預留資源、寫預提交日誌,並返回 yes/no
  2. 協調者彙總:全部 yes → 下發 commit;任一 no/超時 → 下發 rollback
  3. 參與者根據指令提交或回滾,並回執協調者

常見問題:協調者單點、參與者阻塞(長時間持鎖),網路分割時恢復複雜。

sequenceDiagram
  participant C as 協調者(Coordinator)
  participant P1 as 參與者1
  participant P2 as 參與者2

  C->>P1: prepare
  C->>P2: prepare
  P1-->>C: yes/預提交成功
  P2-->>C: yes/預提交成功
  alt 全部yes
    C->>P1: commit
    C->>P2: commit
    P1-->>C: ack
    P2-->>C: ack
  else 任一no/超時
    C->>P1: rollback
    C->>P2: rollback
  end

3.2 三階段提交(3PC)

在 2PC 基礎上增加預提交階段,減少阻塞時間,但仍存在單點故障問題。

3.3 TCC(Try-Confirm-Cancel)

原理

  • Try:嘗試執行業務,預留資源
  • Confirm:確認執行業務,提交資源
  • Cancel:取消執行業務,釋放資源

優點:效能好、無阻塞
缺點:實現複雜、需要業務補償

落地要點(以訂單-庫存-支付為例)

  • Try:建立訂單預狀態、預佔庫存(扣減可用庫存、增加預佔庫存)、預下單支付
  • Confirm(支付成功回呼或非同步確認):訂單變為已支付、庫存從預佔轉正式扣減
  • Cancel(支付失敗/超時):訂單取消、釋放預佔庫存

實現細節:介面冪等(Idempotency)(去重表/唯一業務鍵)、空回滾(Null Rollback)/懸掛處理(Dangling Transaction)、事務日誌記錄與重試任務。

sequenceDiagram
  participant Order as 訂單服務
  participant Inv as 庫存服務
  participant Pay as 支付服務

  rect rgb(230,250,230)
  Note over Order,Inv: Try 階段(預留資源)
  Order->>Inv: Try 預佔庫存
  Order->>Pay: Try 預下單/凍結
  end

  alt 支付成功
    rect rgb(230,230,255)
    Note over Order,Inv: Confirm 階段
    Pay-->>Order: 支付成功回呼
    Order->>Inv: Confirm 扣減庫存
    Order->>Pay: Confirm 確認扣款
    end
  else 失敗/超時
    rect rgb(255,230,230)
    Note over Order,Inv: Cancel 階段
    Order->>Inv: Cancel 釋放預佔
    Order->>Pay: Cancel 解凍/撤銷
    end
  end

3.4 基於訊息的最終一致性

原理

  1. 本地事務執行
  2. 發送訊息到訊息佇列(Message Queue, MQ)
  3. 消費者處理訊息,保證最終一致性

優點:效能好、實現相對簡單
缺點:只能保證最終一致性

3.4.1 基於本地訊息表(Outbox Pattern)

流程:同庫同事務內寫業務資料與 outbox 訊息表 → 後台轉發器輪詢投遞到 MQ → 消費者處理並寫入資料庫 → 發送確認/對帳。

要點:

  • 生產端強一致性(業務+訊息同庫同事務)
  • 轉發冪等(按訊息 ID 投遞、消費去重)
  • 失敗重試與死信佇列(Dead-Letter Queue)、人工對帳修復
flowchart LR
  A[應用/業務服務] -->|同庫同事務| B[(業務表 + Outbox表)]
  B -->|後台轉發器掃描/拉取| MQ[訊息佇列]
  MQ --> C[下游服務]
  C --> D[(消費寫入資料庫/去重表)]

  subgraph 重試與對帳
    E[失敗重投/死信佇列]
    F[對帳/人工修復]
  end
  MQ --> E
  E --> F
3.4.2 基於可靠訊息的最終一致性(常用)

流程:

  1. 業務方向 MQ 申請「預訊息/半訊息」(prepare)
  2. 業務本地提交成功後呼叫 MQ 確認(commit),否則回滾(rollback)
  3. MQ 掛起未確認的半訊息並回查(check)業務方最終狀態,決定提交或丟棄

要點:

  • 依賴 MQ 的事務訊息(Transactional Message)/回查能力(RocketMQ 等)
  • 生產與消費兩端均需冪等處理
sequenceDiagram
  participant Biz as 業務服務
  participant MQ as MQ(事務訊息)
  participant D as 下游服務

  Biz->>MQ: 發送半訊息(Prepare)
  Biz->>Biz: 執行業務本地事務
  alt 成功
    Biz->>MQ: Commit 確認
  else 失敗
    Biz->>MQ: Rollback 撤銷
  end
  MQ->>D: 投遞正式訊息
  D-->>MQ: Ack/重試

  MQ->>Biz: 事務回查(Check) 未確認半訊息
  Biz-->>MQ: 返回最終狀態(提交/回滾)
3.4.3 最大努力通知

流程:事件發生後向下游發起通知(HTTP/MQ),失敗則按策略重試若干次,超過閾值進入人工處理。

適用:對一致性要求相對寬鬆的場景(如簡訊、站內信、積分發放)。

flowchart LR
  A[事件源] --> B{通知}
  B -->|HTTP/MQ| C[下游]
  B --> R1[重試1]
  R1 --> R2[重試2]
  R2 --> R3[重試N]
  R3 --> DLQ[死信/人工補償]
  C --> Idem[去重/冪等處理]

4. 訂單系統中的事務處理

4.1 庫存扣減問題

在訂單系統中,庫存扣減是關鍵操作:

// 庫存扣減示例
func (s *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    // 開啓事務
    tx := global.DB.Begin()
    if tx.Error != nil {
        return nil, status.Error(codes.Internal, "開啓事務失敗")
    }

    // 遍歷所有商品
    for _, goodsInfo := range req.GoodsInvInfo {
        var inv model.Inventory
        // 使用行鎖查詢
        result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
            Where("goods_id = ?", goodsInfo.GoodsId).
            First(&inv)

        // 檢查庫存是否充足
        if inv.Stock < goodsInfo.Num {
            tx.Rollback()
            return nil, status.Error(codes.ResourceExhausted, "庫存不足")
        }

        // 使用樂觀鎖更新庫存
        updateResult := tx.Model(&model.Inventory{}).
            Where("goods_id = ? AND version = ?", goodsInfo.GoodsId, inv.Version).
            Updates(map[string]interface{}{
                "stock":   inv.Stock - goodsInfo.Num,
                "version": inv.Version + 1,
            })
    }

    // 提交事務
    if err := tx.Commit().Error; err != nil {
        return nil, status.Error(codes.Internal, "提交事務失敗")
    }
    return &emptypb.Empty{}, nil
}

4.2 分散式鎖(Distributed Lock)解決並發問題

// 基於Redis的分布式鎖
func (s *InventoryServer) SellWithDistributedLock(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    // 獲取分布式鎖
    lockKey := fmt.Sprintf("inventory_lock_%d", req.GoodsInvInfo[0].GoodsId)
    lock := s.redisClient.NewMutex(lockKey, time.Second*10)

    if err := lock.Lock(); err != nil {
        return nil, status.Error(codes.Internal, "獲取鎖失敗")
    }
    defer lock.Unlock()

    // 執行庫存扣減邏輯
    return s.Sell(ctx, req)
}

5. 業務場景分析

5.1 下單不支付問題

問題:使用者下單後不支付,導致庫存被佔用

解決方案

  1. 訂單超時機制:設定訂單超時時間,超時後自動取消
  2. 庫存預佔:下單時預佔庫存,支付成功後確認扣減
  3. 定時任務(Scheduled Task):定期清理超時訂單,釋放庫存

5.2 支付時庫存不足問題

問題:下單時庫存充足,支付時庫存不足

解決方案

  1. 庫存預佔:下單時預佔庫存,避免超賣
  2. 支付時再次檢查:支付前再次驗證庫存
  3. 補償機制:庫存不足時提供替代方案

6. 最佳實踐

  1. 合理使用事務:避免長事務,減少鎖競爭
  2. 選擇合適的隔離級別:根據業務需求選擇
  3. 使用樂觀鎖(Optimistic Locking):減少鎖競爭,提高並發效能
  4. 實現重試機制:處理臨時性失敗
  5. 監控和告警:及時發現和處理問題

7. 總結

事務和分散式事務是保證資料一致性的重要機制。在微服務架構中,需要根據業務場景選擇合適的分散式事務解決方案,平衡一致性、可用性和效能。訂單系統作為典型的分散式事務場景,需要特別注意庫存扣減、訂單狀態管理等關鍵操作的資料一致性。

總結與思考

TCC 分散式事務總結

總結一下,你要玩兒 TCC 分散式事務的話:

  1. 首先需要選擇某種 TCC 分散式事務框架,各個服務裡就會有這個 TCC 分散式事務框架在運行。
  2. 然後你原本的一個介面,要改造為 3 個邏輯:Try-Confirm-Cancel。

TCC 流程:

  • 先是服務呼叫鏈路依次執行 Try 邏輯
  • 如果都正常的話,TCC 分散式事務框架推進執行 Confirm 邏輯,完成整個事務
  • 如果某個服務的 Try 邏輯有問題,TCC 分散式事務框架感知到之後就會推進執行各個服務的 Cancel 邏輯,撤銷之前執行的各種操作
  • 這就是所謂的 TCC 分散式事務

TCC 分散式事務

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4786

(0)
Walker的頭像Walker
上一篇 2025年11月25日 13:00
下一篇 2025年11月25日 11:00

相關推薦

  • 深入理解ES6 001【學習筆記】

    區塊作用域繫結 之前的變數宣告 `var` 無論在哪裡宣告,都會被視為作用域頂部宣告。由於函式是一等公民,因此順序通常是 `function 函式名稱()`、`var 變數`。 區塊宣告 區塊宣告用於宣告在指定區塊的作用域之外無法存取的變數。區塊作用域存在於: 函式內部 區塊中(字元 `{` 和 `}` 之間的區域) 暫時性死區 JavaScript 引擎在掃描程式碼發現變數宣告時,要麼會將它們提升至作…

    個人 2025年3月8日
    1.6K00
  • 深入理解ES6 011【學習筆記】

    Promise與異步程式設計 因為執行引擎是單執行緒的,所以需要追蹤即將執行的程式碼,那些程式碼會被放在一個任務佇列中,每當一段程式碼準備執行時,都會被新增到任務佇列中,每當引擎中的一段程式碼結束執行時,事件迴圈會執行佇列中的下一個任務。 Promise 相當於異步操作結果的佔位符,它不會去訂閱一個事件,也不會傳遞一個回呼函式給目標函式,而是讓函式回傳一個 Promise,就像這樣…

    個人 2025年3月8日
    1.1K00
  • 深入理解ES6 007【學習筆記】

    Set集合與Map集合 在js中有一個in運算子,其不需要讀取物件的值就能判斷屬性在物件中是否存在,如果存在就回傳true。但是in運算子也會檢查物件的原型,只有當物件原型為null時使用這個方法才比較穩妥。 Set集合 let set = new Set() set.add(5) set.add("5") console.log(s…

    個人 2025年3月8日
    1.2K00
  • Go工程師體系課 017【學習筆記】

    限流、熔斷與降級入門(含 Sentinel 實戰) 結合課件第 3 章(3-1 ~ 3-9)的影片要點,整理一套面向初學者的服務保護指南,幫助理解「為什麼需要限流、熔斷和降級」,以及如何用 Sentinel 快速上手。 學習路線速覽 3-1 理解服務雪崩與限流、熔斷、降級的背景 3-2 Sentinel 與 Hystrix 對比,明確技術選型 3-3 Sen…

    個人 2025年11月25日
    18000
  • 深入理解ES6 013【學習筆記】

    用模組封裝程式碼 JavaScript 使用「共享一切」的方式載入程式碼,這是該語言中最容易出錯且令人感到困惑的地方。其他語言使用諸如套件(package)之類的概念來定義程式碼作用域。在 ES6 以前,在應用程式的每一個 JavaScript 檔案中定義的一切都共享一個全域作用域。隨著網頁應用程式變得更加複雜,JavaScript 程式碼的使用量也開始增長,這種做法會引起問題,例如命名衝突和安全性問題。ES6 的一個目標是解決作用域問題…

    個人 2025年3月8日
    1.1K00
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹