分布式事務(wù)解決方案:深入理解 TCC 模式
在分布式系統(tǒng)中,事務(wù)處理一直是一個復(fù)雜的話題。想象一下,當你在網(wǎng)上商城購物時,整個過程涉及:
- 訂單系統(tǒng)創(chuàng)建訂單
- 庫存系統(tǒng)扣減庫存
- 支付系統(tǒng)完成支付
- 積分系統(tǒng)增加積分
這些操作分布在不同的服務(wù)中,如何保證它們要么全部成功,要么全部失敗?這就是分布式事務(wù)需要解決的問題。
一、分布式事務(wù)的挑戰(zhàn)
1.傳統(tǒng)事務(wù)的局限
在單體應(yīng)用中,我們習慣使用數(shù)據(jù)庫的 ACID 事務(wù):
@Transactional
public void createOrder(Order order) {
// 創(chuàng)建訂單
orderRepository.save(order);
// 扣減庫存
inventoryRepository.deduct(order.getProductId(), order.getQuantity());
// 扣減余額
accountRepository.deduct(order.getUserId(), order.getAmount());
}
但在分布式環(huán)境下,這種方式行不通了,因為:
- 跨多個數(shù)據(jù)庫
- 跨多個服務(wù)
- 網(wǎng)絡(luò)可能失敗
- 服務(wù)可能宕機
2.CAP 理論的限制
在分布式系統(tǒng)中,我們不得不在以下三個特性中做出選擇:
- 一致性(Consistency)
- 可用性(Availability)
- 分區(qū)容錯性(Partition tolerance)
二、TCC 模式介紹
1.什么是 TCC?
TCC(Try-Confirm-Cancel)是一種補償性事務(wù)模式,它將一個完整的業(yè)務(wù)操作分為二步完成:
(1) Try: 嘗試執(zhí)行業(yè)務(wù)
- 完成所有業(yè)務(wù)檢查
- 預(yù)留必要的業(yè)務(wù)資源
(2) Confirm: 確認執(zhí)行業(yè)務(wù)
- 真正執(zhí)行業(yè)務(wù)
- 不做任何業(yè)務(wù)檢查
- 只使用 Try 階段預(yù)留的資源
(3) Cancel: 取消執(zhí)行業(yè)務(wù)
- 釋放 Try 階段預(yù)留的資源
- 回滾操作
來源:seata
2.TCC 示例:訂單支付流程
讓我們通過一個具體的訂單支付場景來理解 TCC:
// 訂單服務(wù)的 TCC 實現(xiàn)
public class OrderTccService {
// Try: 創(chuàng)建預(yù)訂單
@Transactional
public void tryCreate(Order order) {
// 檢查訂單參數(shù)
validateOrder(order);
// 創(chuàng)建預(yù)訂單
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);
}
}
// 庫存服務(wù)的 TCC 實現(xiàn)
public class InventoryTccService {
// Try: 凍結(jié)庫存
@Transactional
public void tryDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
// 檢查并凍結(jié)庫存
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);
}
}
// 支付服務(wù)的 TCC 實現(xiàn)
public class PaymentTccService {
// Try: 凍結(jié)金額
@Transactional
public void tryDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
// 檢查并凍結(jié)金額
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);
}
}
3.TCC 事務(wù)協(xié)調(diào)器
為了協(xié)調(diào)整個 TCC 流程,我們需要一個事務(wù)協(xié)調(diào)器:
@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. 創(chuàng)建預(yù)訂單
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. 恢復(fù)庫存
inventoryService.cancelDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 恢復(fù)余額
paymentService.cancelDeduct(
order.getUserId(),
order.getAmount()
);
throw new OrderCreateFailedException(e);
}
}
}
三、TCC 實現(xiàn)要點
1. 業(yè)務(wù)模型設(shè)計
在實現(xiàn) TCC 時,業(yè)務(wù)模型需要考慮預(yù)留資源的狀態(tài):
public class Inventory {
private String productId;
private int total; // 總庫存
private int available; // 可用庫存
private int frozen; // 凍結(jié)庫存
}
public class Account {
private String userId;
private BigDecimal total; // 總額
private BigDecimal available; // 可用余額
private BigDecimal frozen; // 凍結(jié)金額
}
圖 3: TCC 中的資源狀態(tài)變化,來源 seata
2. 冪等性設(shè)計
所有操作都需要保證冪等,因為在網(wǎng)絡(luò)異常時可能會重試:
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 檢查是否已經(jīng)執(zhí)行過
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 執(zhí)行業(yè)務(wù)邏輯
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
// 記錄執(zhí)行日志
tccLogRepository.s
3. 防懸掛設(shè)計
(1) 為什么需要防懸掛?
在分布式系統(tǒng)中,網(wǎng)絡(luò)延遲、服務(wù)故障等原因可能導(dǎo)致一個奇怪的現(xiàn)象,Cancel 操作比 Try 操作先執(zhí)行。這就是所謂的"懸掛"問題。具體場景如下:
事務(wù)管理器在調(diào)用 TCC 服務(wù)的一階段 Try 操作時事務(wù)時,由于網(wǎng)絡(luò)擁堵,Try 請求沒有及時到達,事務(wù)管理器超時后,發(fā)起了 Cancel 請求完成后,此時原來的 Try 請求才到達,如果在執(zhí)行這個延遲的 Try 請求,將導(dǎo)致資源被錯誤鎖定
*圖: TCC 懸掛問題示意圖,來源:seata
(2) 解決方案
核心思路是記錄每個事務(wù)的執(zhí)行狀態(tài),并在執(zhí)行 Try 操作前進行檢查:
@Service
public class TccTransactionService {
@Autowired
private TccLogRepository tccLogRepository;
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 1. 檢查是否已經(jīng)被 Cancel
if (tccLogRepository.existsByXidAndPhase(xid, "cancel")) {
throw new TransactionCancelledException("Transaction already cancelled");
}
// 2. 檢查是否已經(jīng)執(zhí)行過 Try (冪等性檢查)
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 3. 執(zhí)行業(yè)務(wù)邏輯
Account account = accountRepository.findById(userId);
if (account.getAvailable().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
// 4. 記錄執(zhí)行日志
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
tccLogRepository.save(new TccLog(xid, "try"));
}
}
4. 超時處理
(1) 為什么需要超時處理?
在分布式環(huán)境下,超時是不可避免的,可能由于以下原因?qū)е?
- 網(wǎng)絡(luò)延遲或故障
- 服務(wù)器負載過高
- 服務(wù)進程崩潰
- 死鎖
如果不處理超時,會造成嚴重后果:
- 資源被無限期鎖定
- 事務(wù)無法正常結(jié)束
- 系統(tǒng)可用性降低
- 用戶體驗變差
(2) 超時處理機制
定時掃描超時事務(wù):
@Component
public class TccTimeoutChecker {
@Autowired
private TccLogRepository tccLogRepository;
@Autowired
private TccTransactionHandler transactionHandler;
@Scheduled(fixedRate = 60000) // 每分鐘執(zhí)行一次
public void checkTimeout() {
// 1. 查找超時的事務(wù)
List<TccLog> timeoutLogs = tccLogRepository
.findByPhaseAndCreateTimeBefore(
"try",
LocalDateTime.now().minusMinutes(5)
);
for (TccLog log : timeoutLogs) {
try {
// 2. 執(zhí)行 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
}
監(jiān)控和告警:
@Component
public class TccMonitor {
@Autowired
private AlertService alertService;
public void onTransactionTimeout(String xid) {
// 記錄監(jiān)控指標
MetricsRegistry.counter("tcc.timeout").increment();
// 發(fā)送告警
alertService.sendAlert(
"TCC Transaction Timeout",
String.format("Transaction %s timeout", xid),
AlertLevel.WARNING
);
}
public void onCancelFailed(String xid, Exception e) {
// 記錄監(jiān)控指標
MetricsRegistry.counter("tcc.cancel.failed").increment();
// 發(fā)送告警
alertService.sendAlert(
"TCC Cancel Failed",
String.format("Transaction %s cancel failed: %s", xid, e.getMessage()),
AlertLevel.ERROR
);
}
}
(3) 最佳實踐
超時時間設(shè)置:
- 根據(jù)業(yè)務(wù)特點設(shè)置合理的超時時間
- 考慮網(wǎng)絡(luò)延遲和服務(wù)響應(yīng)時間
- 為復(fù)雜業(yè)務(wù)預(yù)留足夠的處理時間
- 不同類型的事務(wù)可以設(shè)置不同的超時時間
重試機制:
- 實現(xiàn)指數(shù)退避算法
- 設(shè)置最大重試次數(shù)
- 合理的重試間隔
- 重試時要考慮冪等性
監(jiān)控和告警:
- 監(jiān)控超時事務(wù)數(shù)量
- 監(jiān)控 Cancel 操作的成功率
- 監(jiān)控資源占用情況
- 設(shè)置合理的告警閾值
人工干預(yù):
- 提供管理后臺
- 支持手動觸發(fā) Cancel
- 提供事務(wù)狀態(tài)查詢
- 記錄詳細的操作日志
通過這些機制的組合,我們可以構(gòu)建一個健壯的 TCC 事務(wù)處理系統(tǒng),能夠:
- 及時發(fā)現(xiàn)并處理超時事務(wù)
- 防止資源被長期鎖定
- 提供完善的監(jiān)控和運維能力
- 在出現(xiàn)問題時及時告警并支持人工介入
四、最佳實踐
資源預(yù)留:
- Try 階段要預(yù)留足夠的資源
- 預(yù)留資源要考慮并發(fā)情況
- 預(yù)留時間要合理設(shè)置
狀態(tài)機制:
- 明確定義每個階段的狀態(tài)
- 狀態(tài)轉(zhuǎn)換要有清晰的規(guī)則
- 保存狀態(tài)轉(zhuǎn)換歷史
異常處理:
- 所有異常都要有補償措施
- 補償操作要能重試
- 重試策略要合理設(shè)置
監(jiān)控告警:
- 監(jiān)控每個階段的執(zhí)行情況
- 設(shè)置合理的告警閾值
- 提供人工干預(yù)的接口
五、適用場景
TCC 模式適合:
- 強一致性要求高的業(yè)務(wù)
- 實時性要求高的場景
- 有資源鎖定需求的操作
不適合:
- 業(yè)務(wù)邏輯簡單的場景
- 對性能要求特別高的場景
- 補償成本過高的業(yè)務(wù)
六、結(jié)論
TCC 是一種強大的分布式事務(wù)解決方案,它通過巧妙的補償機制來保證事務(wù)的一致性。雖然實現(xiàn)較為復(fù)雜,但在某些場景下是不可替代的選擇。
關(guān)鍵是要:
- 理解業(yè)務(wù)場景
- 合理設(shè)計補償邏輯
- 做好異常處理
- 重視監(jiān)控告警
通過合理使用 TCC 模式,我們可以在分布式系統(tǒng)中實現(xiàn)可靠的事務(wù)處理。