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://www.walker-learn.xyz/archives/4786

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

相關推薦

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

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

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

    改進的陣列功能 new Array() 的怪異行為,當建構函式傳入一個數值型的值,那麼陣列的 length 屬性會被設為該值;如果傳入多個值,此時無論這些值是不是數值型的,都會變為陣列的元素。這個特性令人困惑,你不可能總是注意傳入資料的類型,所以存在一定的風險。 Array.of() 無論傳多少個參數,不存在單一數值的特例(一個參數且數值型),總是返回包含所有參數…

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

    轉型 想在短時間系統轉到Go工程的理由 提高CRUD,無自研框架經驗 提升技術深度,做專、做精需求的同學 進階工程化,擁有良好開發規範和管理能力的 工程化的重要性 高階開發的期望 良好的程式碼規範 深入底層原理 熟悉架構 熟悉k8s的基礎架構 擴展知識廣度,知識的深度,規範的開發體系 四個大的階段 Go語言基礎 微服務開發的(電商專案實戰) 自研微服務 自研然後重…

    個人 2025年11月25日
    37500
  • 熱愛運動,挑戰極限,擁抱自然

    熱愛 在這個快節奏的時代,我們被工作、生活的壓力所包圍,常常忽略了身體的需求。而運動,不只是一種健身方式,更是一種釋放自我、挑戰極限、與自然共舞的生活態度。無論是滑雪、攀岩、衝浪,還是跑步、騎行、瑜伽,每一種運動都能讓我們找到內心的激情,感受到生命的躍動。 運動是一場自我挑戰。挑戰極限,不只是職業運動員的專屬,而是每一個熱愛運動的人都可以追求的目標。它可…

    個人 2025年2月26日
    1.5K00
  • 深入理解ES6 003【學習筆記】

    函數參數預設值,以及一些關於 arguments 物件,如何使用運算式作為參數、參數的暫時性死區。 以前設定預設值總是利用在含有邏輯或運算子的運算式中,前一個值是 false 時,總是回傳後面那個值,但如果我們給參數傳入 0 時,就會有些麻煩。 需要去驗證一下型別 function makeRequest(url,timeout,callback){ timeout = t…

    個人 2025年3月8日
    1.3K00