分佈式事務解決方案:深入理解 TCC 模式
引言
在分佈式系統中, 事務處理一直是一個複雜的話題。想象一下, 當你在網上商城購物時, 整個過程涉及:
-
訂單系統創建訂單
-
庫存系統扣減庫存
-
支付系統完成支付
-
積分系統增加積分
這些操作分佈在不同的服務中, 如何保證它們要麼全部成功, 要麼全部失敗? 這就是分佈式事務需要解決的問題。
分佈式事務的挑戰
傳統事務的侷限
在單體應用中, 我們習慣使用數據庫的 ACID 事務:
@Transactional
public void createOrder(Order order) {
// 創建訂單
orderRepository.save(order);
// 扣減庫存
inventoryRepository.deduct(order.getProductId(), order.getQuantity());
// 扣減餘額
accountRepository.deduct(order.getUserId(), order.getAmount());
}
但在分佈式環境下, 這種方式行不通了, 因爲:
-
跨多個數據庫
-
跨多個服務
-
網絡可能失敗
-
服務可能宕機
CAP 理論的限制
在分佈式系統中, 我們不得不在以下三個特性中做出選擇:
-
一致性 (Consistency)
-
可用性 (Availability)
-
分區容錯性 (Partition tolerance)
TCC 模式介紹
什麼是 TCC?
TCC(Try-Confirm-Cancel) 是一種補償性事務模式, 它將一個完整的業務操作分爲二步完成:
-
Try: 嘗試執行業務
-
完成所有業務檢查
-
預留必要的業務資源
-
-
Confirm: 確認執行業務
-
真正執行業務
-
不做任何業務檢查
-
只使用 Try 階段預留的資源
-
-
Cancel: 取消執行業務
-
釋放 Try 階段預留的資源
-
回滾操作
-
來源:seata
TCC 示例:訂單支付流程
讓我們通過一個具體的訂單支付場景來理解 TCC:
// 訂單服務的 TCC 實現
public class OrderTccService {
// Try: 創建預訂單
@Transactional
public void tryCreate(Order order) {
// 檢查訂單參數
validateOrder(order);
// 創建預訂單
order.setStatus(OrderStatus.TRYING);
orderRepository.save(order);
}
// Confirm: 確認訂單
@Transactional
public void confirmCreate(String orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
}
// Cancel: 取消訂單
@Transactional
public void cancelCreate(String orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// 庫存服務的 TCC 實現
public class InventoryTccService {
// Try: 凍結庫存
@Transactional
public void tryDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
// 檢查並凍結庫存
if (inventory.getAvailable() < quantity) {
throw new InsufficientInventoryException();
}
inventory.setFrozen(inventory.getFrozen() + quantity);
inventory.setAvailable(inventory.getAvailable() - quantity);
inventoryRepository.save(inventory);
}
// Confirm: 確認扣減
@Transactional
public void confirmDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventoryRepository.save(inventory);
}
// Cancel: 解凍庫存
@Transactional
public void cancelDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventory.setAvailable(inventory.getAvailable() + quantity);
inventoryRepository.save(inventory);
}
}
// 支付服務的 TCC 實現
public class PaymentTccService {
// Try: 凍結金額
@Transactional
public void tryDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
// 檢查並凍結金額
if (account.getAvailable().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
}
// Confirm: 確認支付
@Transactional
public void confirmDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().subtract(amount));
accountRepository.save(account);
}
// Cancel: 解凍金額
@Transactional
public void cancelDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().subtract(amount));
account.setAvailable(account.getAvailable().add(amount));
accountRepository.save(account);
}
}
TCC 事務協調器
爲了協調整個 TCC 流程, 我們需要一個事務協調器:
@Service
public class OrderTccCoordinator {
@Autowired
private OrderTccService orderService;
@Autowired
private InventoryTccService inventoryService;
@Autowired
private PaymentTccService paymentService;
public void createOrder(Order order) {
String xid = generateTransactionId();
try {
// ==== Try 階段 ====
// 1. 創建預訂單
orderService.tryCreate(order);
// 2. 嘗試扣減庫存
inventoryService.tryDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 嘗試扣減餘額
paymentService.tryDeduct(
order.getUserId(),
order.getAmount()
);
// ==== Confirm 階段 ====
// 1. 確認訂單
orderService.confirmCreate(order.getId());
// 2. 確認庫存扣減
inventoryService.confirmDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 確認支付
paymentService.confirmDeduct(
order.getUserId(),
order.getAmount()
);
} catch (Exception e) {
// ==== Cancel 階段 ====
// 1. 取消訂單
orderService.cancelCreate(order.getId());
// 2. 恢復庫存
inventoryService.cancelDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 恢復餘額
paymentService.cancelDeduct(
order.getUserId(),
order.getAmount()
);
throw new OrderCreateFailedException(e);
}
}
}
TCC 實現要點
1. 業務模型設計
在實現 TCC 時, 業務模型需要考慮預留資源的狀態:
public class Inventory {
private String productId;
private int total; // 總庫存
private int available; // 可用庫存
private int frozen; // 凍結庫存
}
public class Account {
private String userId;
private BigDecimal total; // 總額
private BigDecimal available; // 可用餘額
private BigDecimal frozen; // 凍結金額
}
圖 3: TCC 中的資源狀態變化,來源 seata
2. 冪等性設計
所有操作都需要保證冪等, 因爲在網絡異常時可能會重試:
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 檢查是否已經執行過
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 執行業務邏輯
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
// 記錄執行日誌
tccLogRepository.save(new TccLog(xid, "try"));
}
3. 防懸掛設計
爲什麼需要防懸掛?
在分佈式系統中, 網絡延遲、服務故障等原因可能導致一個奇怪的現象,Cancel 操作比 Try 操作先執行。這就是所謂的 "懸掛" 問題。具體場景如下:
事務管理器在調用 TCC 服務的一階段 Try 操作時事務時,由於網絡擁堵, Try 請求沒有及時到達,事務管理器超時後, 發起了 Cancel 請求完成後,此時原來的 Try 請求才到達,如果在執行這個延遲的 Try 請求, 將導致資源被錯誤鎖定
- 圖: TCC 懸掛問題示意圖,來源:seata
解決方案
核心思路是記錄每個事務的執行狀態, 並在執行 Try 操作前進行檢查:
@Service
public class TccTransactionService {
@Autowired
private TccLogRepository tccLogRepository;
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 1. 檢查是否已經被 Cancel
if (tccLogRepository.existsByXidAndPhase(xid, "cancel")) {
throw new TransactionCancelledException("Transaction already cancelled");
}
// 2. 檢查是否已經執行過 Try (冪等性檢查)
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 3. 執行業務邏輯
Account account = accountRepository.findById(userId);
if (account.getAvailable().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
// 4. 記錄執行日誌
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
tccLogRepository.save(new TccLog(xid, "try"));
}
}
4. 超時處理
爲什麼需要超時處理?
在分佈式環境下, 超時是不可避免的, 可能由於以下原因導致:
-
網絡延遲或故障
-
服務器負載過高
-
服務進程崩潰
-
死鎖
如果不處理超時, 會造成嚴重後果:
-
資源被無限期鎖定
-
事務無法正常結束
-
系統可用性降低
-
用戶體驗變差
超時處理機制
- 定時掃描超時事務
@Component
public class TccTimeoutChecker {
@Autowired
private TccLogRepository tccLogRepository;
@Autowired
private TccTransactionHandler transactionHandler;
@Scheduled(fixedRate = 60000) // 每分鐘執行一次
public void checkTimeout() {
// 1. 查找超時的事務
List<TccLog> timeoutLogs = tccLogRepository
.findByPhaseAndCreateTimeBefore(
"try",
LocalDateTime.now().minusMinutes(5)
);
for (TccLog log : timeoutLogs) {
try {
// 2. 執行 Cancel 操作
transactionHandler.cancelTransaction(log.getXid());
// 3. 記錄取消日誌
log.setPhase("cancel");
log.setUpdateTime(LocalDateTime.now());
tccLogRepository.save(log);
} catch (Exception e) {
// 4. 記錄錯誤,可能需要人工介入
errorLogger.log(
"Failed to cancel timeout transaction: " + log.getXid(),
e
);
}
}
}
}
- 超時配置管理
@Configuration
public class TccConfig {
@Value("${tcc.transaction.timeout:60000}")
private long transactionTimeout; // 默認60秒
@Value("${tcc.check.interval:5000}")
private long checkInterval; // 默認5秒
@Value("${tcc.retry.max:3}")
private int maxRetryCount; // 默認重試3次
@Value("${tcc.retry.interval:1000}")
private long retryInterval; // 默認重試間隔1秒
// getter and setter
}
- 監控和告警
@Component
public class TccMonitor {
@Autowired
private AlertService alertService;
public void onTransactionTimeout(String xid) {
// 記錄監控指標
MetricsRegistry.counter("tcc.timeout").increment();
// 發送告警
alertService.sendAlert(
"TCC Transaction Timeout",
String.format("Transaction %s timeout", xid),
AlertLevel.WARNING
);
}
public void onCancelFailed(String xid, Exception e) {
// 記錄監控指標
MetricsRegistry.counter("tcc.cancel.failed").increment();
// 發送告警
alertService.sendAlert(
"TCC Cancel Failed",
String.format("Transaction %s cancel failed: %s", xid, e.getMessage()),
AlertLevel.ERROR
);
}
}
最佳實踐
-
超時時間設置
-
根據業務特點設置合理的超時時間
-
考慮網絡延遲和服務響應時間
-
爲複雜業務預留足夠的處理時間
-
不同類型的事務可以設置不同的超時時間
-
-
重試機制
-
實現指數退避算法
-
設置最大重試次數
-
合理的重試間隔
-
重試時要考慮冪等性
-
-
監控和告警
-
監控超時事務數量
-
監控 Cancel 操作的成功率
-
監控資源佔用情況
-
設置合理的告警閾值
-
-
人工干預
-
提供管理後臺
-
支持手動觸發 Cancel
-
提供事務狀態查詢
-
記錄詳細的操作日誌
-
通過這些機制的組合, 我們可以構建一個健壯的 TCC 事務處理系統, 能夠:
-
及時發現並處理超時事務
-
防止資源被長期鎖定
-
提供完善的監控和運維能力
-
在出現問題時及時告警並支持人工介入
最佳實踐
-
資源預留
-
Try 階段要預留足夠的資源
-
預留資源要考慮併發情況
-
預留時間要合理設置
-
-
狀態機制
-
明確定義每個階段的狀態
-
狀態轉換要有清晰的規則
-
保存狀態轉換歷史
-
-
異常處理
-
所有異常都要有補償措施
-
補償操作要能重試
-
重試策略要合理設置
-
-
監控告警
-
監控每個階段的執行情況
-
設置合理的告警閾值
-
提供人工干預的接口
-
適用場景
TCC 模式適合:
-
強一致性要求高的業務
-
實時性要求高的場景
-
有資源鎖定需求的操作
不適合:
-
業務邏輯簡單的場景
-
對性能要求特別高的場景
-
補償成本過高的業務
結論
TCC 是一種強大的分佈式事務解決方案, 它通過巧妙的補償機制來保證事務的一致性。雖然實現較爲複雜, 但在某些場景下是不可替代的選擇。
關鍵是要:
-
理解業務場景
-
合理設計補償邏輯
-
做好異常處理
-
重視監控告警
通過合理使用 TCC 模式, 我們可以在分佈式系統中實現可靠的事務處理。
你好,我是架構成長指南作者蝸牛,10 餘年開發經驗、從事過金融、區塊鏈、物流地產等行業,
做過普通開發、架構師、技術 Leader,此公衆號會分享一些工作中的技術知識,歡迎關注!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/evrC5SC6RyoQwM76JahgcQ