訂單事務
- 先扣庫存、後扣庫存都會對庫存和訂單產生影響,所以要使用分散式事務(Distributed Transaction)
- 業務(下單不支付)業務問題
- 支付成功再扣減(下單了,支付時沒庫存了)
- 訂單扣減,不支付(訂單超時歸還)【常用方式】
事務與分散式事務
1. 什麼是事務?
事務(Transaction)是資料庫管理系統中的一個重要概念,它是一組資料庫操作的集合,這些操作要嘛全部成功執行,要嘛全部失敗回滾。
1.1 事務的 ACID 特性
- 原子性(Atomicity):事務中的所有操作要嘛全部成功,要嘛全部失敗,不存在部分成功的情況
- 一致性(Consistency):事務執行前後,資料庫從一個一致狀態轉換到另一個一致狀態
- 隔離性(Isolation):並行執行的事務之間相互隔離,一個事務的執行不應影響其他事務
- 持久性(Durability):事務一旦提交,其結果就永久保存在資料庫中
1.2 事務的隔離級別
- 讀未提交(Read Uncommitted):最低級別,可能讀到髒資料
- 讀已提交(Read Committed):只能讀到已提交的資料
- 可重複讀(Repeatable Read):同一事務中多次讀取結果一致
- 串行化(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)
原理:
- 準備階段:協調者詢問所有參與者是否可以提交
- 提交階段:根據參與者回應決定提交或回滾
優點:強一致性
缺點:效能差、單點故障、阻塞問題
流程細化(示意):
- 協調者向參與者發送
prepare請求,各參與者預留資源、寫預提交日誌,並返回 yes/no - 協調者彙總:全部 yes → 下發
commit;任一 no/超時 → 下發rollback - 參與者根據指令提交或回滾,並回執協調者
常見問題:協調者單點、參與者阻塞(長時間持鎖),網路分割時恢復複雜。
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 基於訊息的最終一致性
原理:
- 本地事務執行
- 發送訊息到訊息佇列(Message Queue, MQ)
- 消費者處理訊息,保證最終一致性
優點:效能好、實現相對簡單
缺點:只能保證最終一致性
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 基於可靠訊息的最終一致性(常用)
流程:
- 業務方向 MQ 申請「預訊息/半訊息」(prepare)
- 業務本地提交成功後呼叫 MQ 確認(commit),否則回滾(rollback)
- 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 下單不支付問題
問題:使用者下單後不支付,導致庫存被佔用
解決方案:
- 訂單超時機制:設定訂單超時時間,超時後自動取消
- 庫存預佔:下單時預佔庫存,支付成功後確認扣減
- 定時任務(Scheduled Task):定期清理超時訂單,釋放庫存
5.2 支付時庫存不足問題
問題:下單時庫存充足,支付時庫存不足
解決方案:
- 庫存預佔:下單時預佔庫存,避免超賣
- 支付時再次檢查:支付前再次驗證庫存
- 補償機制:庫存不足時提供替代方案
6. 最佳實踐
- 合理使用事務:避免長事務,減少鎖競爭
- 選擇合適的隔離級別:根據業務需求選擇
- 使用樂觀鎖(Optimistic Locking):減少鎖競爭,提高並發效能
- 實現重試機制:處理臨時性失敗
- 監控和告警:及時發現和處理問題
7. 總結
事務和分散式事務是保證資料一致性的重要機制。在微服務架構中,需要根據業務場景選擇合適的分散式事務解決方案,平衡一致性、可用性和效能。訂單系統作為典型的分散式事務場景,需要特別注意庫存扣減、訂單狀態管理等關鍵操作的資料一致性。
總結與思考
TCC 分散式事務總結
總結一下,你要玩兒 TCC 分散式事務的話:
- 首先需要選擇某種 TCC 分散式事務框架,各個服務裡就會有這個 TCC 分散式事務框架在運行。
- 然後你原本的一個介面,要改造為 3 個邏輯:Try-Confirm-Cancel。
TCC 流程:
- 先是服務呼叫鏈路依次執行 Try 邏輯
- 如果都正常的話,TCC 分散式事務框架推進執行 Confirm 邏輯,完成整個事務
- 如果某個服務的 Try 邏輯有問題,TCC 分散式事務框架感知到之後就會推進執行各個服務的 Cancel 邏輯,撤銷之前執行的各種操作
- 這就是所謂的 TCC 分散式事務
TCC 分散式事務
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4786