分佈式事務解決方案:深入理解 TCC 模式

引言

在分佈式系統中, 事務處理一直是一個複雜的話題。想象一下, 當你在網上商城購物時, 整個過程涉及:

這些操作分佈在不同的服務中, 如何保證它們要麼全部成功, 要麼全部失敗? 這就是分佈式事務需要解決的問題。

分佈式事務的挑戰

傳統事務的侷限

在單體應用中, 我們習慣使用數據庫的 ACID 事務:

@Transactional
public void createOrder(Order order) {
    // 創建訂單
    orderRepository.save(order);
    // 扣減庫存
    inventoryRepository.deduct(order.getProductId(), order.getQuantity());
    // 扣減餘額
    accountRepository.deduct(order.getUserId(), order.getAmount());
}

但在分佈式環境下, 這種方式行不通了, 因爲:

  1. 跨多個數據庫

  2. 跨多個服務

  3. 網絡可能失敗

  4. 服務可能宕機

CAP 理論的限制

在分佈式系統中, 我們不得不在以下三個特性中做出選擇:

TCC 模式介紹

什麼是 TCC?

TCC(Try-Confirm-Cancel) 是一種補償性事務模式, 它將一個完整的業務操作分爲二步完成:

  1. Try: 嘗試執行業務

    • 完成所有業務檢查

    • 預留必要的業務資源

  2. Confirm: 確認執行業務

    • 真正執行業務

    • 不做任何業務檢查

    • 只使用 Try 階段預留的資源

  3. 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 請求, 將導致資源被錯誤鎖定

解決方案

核心思路是記錄每個事務的執行狀態, 並在執行 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. 超時處理

爲什麼需要超時處理?

在分佈式環境下, 超時是不可避免的, 可能由於以下原因導致:

如果不處理超時, 會造成嚴重後果:

超時處理機制

  1. 定時掃描超時事務
@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
                );
            }
        }
    }
}
  1. 超時配置管理
@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
}
  1. 監控和告警
@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
        );
    }
}

最佳實踐

  1. 超時時間設置

    • 根據業務特點設置合理的超時時間

    • 考慮網絡延遲和服務響應時間

    • 爲複雜業務預留足夠的處理時間

    • 不同類型的事務可以設置不同的超時時間

  2. 重試機制

    • 實現指數退避算法

    • 設置最大重試次數

    • 合理的重試間隔

    • 重試時要考慮冪等性

  3. 監控和告警

    • 監控超時事務數量

    • 監控 Cancel 操作的成功率

    • 監控資源佔用情況

    • 設置合理的告警閾值

  4. 人工干預

    • 提供管理後臺

    • 支持手動觸發 Cancel

    • 提供事務狀態查詢

    • 記錄詳細的操作日誌

通過這些機制的組合, 我們可以構建一個健壯的 TCC 事務處理系統, 能夠:

最佳實踐

  1. 資源預留

    • Try 階段要預留足夠的資源

    • 預留資源要考慮併發情況

    • 預留時間要合理設置

  2. 狀態機制

    • 明確定義每個階段的狀態

    • 狀態轉換要有清晰的規則

    • 保存狀態轉換歷史

  3. 異常處理

    • 所有異常都要有補償措施

    • 補償操作要能重試

    • 重試策略要合理設置

  4. 監控告警

    • 監控每個階段的執行情況

    • 設置合理的告警閾值

    • 提供人工干預的接口

適用場景

TCC 模式適合:

  1. 強一致性要求高的業務

  2. 實時性要求高的場景

  3. 有資源鎖定需求的操作

不適合:

  1. 業務邏輯簡單的場景

  2. 對性能要求特別高的場景

  3. 補償成本過高的業務

結論

TCC 是一種強大的分佈式事務解決方案, 它通過巧妙的補償機制來保證事務的一致性。雖然實現較爲複雜, 但在某些場景下是不可替代的選擇。

關鍵是要:

通過合理使用 TCC 模式, 我們可以在分佈式系統中實現可靠的事務處理。

你好,我是架構成長指南作者蝸牛,10 餘年開發經驗、從事過金融、區塊鏈、物流地產等行業,

做過普通開發、架構師、技術 Leader,此公衆號會分享一些工作中的技術知識,歡迎關注!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/evrC5SC6RyoQwM76JahgcQ