一、TCC 分布式事务详解:从概念到核心原理
1.1 为什么需要 TCC?分布式事务的痛点
在传统单体应用架构下,我们可以依赖数据库的 ACID(原子性、一致性、隔离性、持久性)事务特性来保证数据一致性。例如银行的转账操作,可以在一个数据库事务中同时完成转出账户扣款和转入账户加款。
但随着系统演进到微服务架构,一个业务流程往往需要跨多个服务协同完成。以典型的电商下单场景为例:
- 订单服务:创建订单记录
- 库存服务:扣减商品库存
- 支付服务:处理用户支付
- 物流服务:生成配送单
这种跨服务的事务一致性无法通过单一数据库的事务来保证,由此产生了分布式事务问题。传统的分布式事务解决方案存在明显局限:
2PC(两阶段提交)的问题
- 同步阻塞:所有参与者在投票阶段必须等待协调者指令,期间资源被锁定
- 单点故障:协调者宕机会导致整个系统阻塞
- 数据不一致:在第二阶段,如果部分参与者未收到提交指令,会导致部分提交部分未提交
- 性能瓶颈:在高并发场景下,事务处理吞吐量显著下降
3PC(三阶段提交)的改进与局限
3PC 通过引入预提交阶段来减少阻塞时间:
- 询问阶段(CanCommit)
- 预提交阶段(PreCommit)
- 提交阶段(DoCommit)
虽然解决了部分阻塞问题,但:
- 系统复杂度显著增加
- 网络分区时仍可能出现数据不一致
- 实现成本高,实际应用较少
1.2 TCC 的核心定义与原理
TCC 模式详解
TCC(Try-Confirm-Cancel)是一种业务层面的分布式事务解决方案,其核心在于将业务操作拆分为三个可管理的阶段:
Try 阶段:业务检查和资源预留
- 典型操作:
- 订单服务:创建"待确认"状态的订单
- 库存服务:将库存从"可用"转为"预占"状态
- 支付服务:冻结用户部分余额
- 关键特性:
- 幂等设计:重复调用不会产生副作用
- 可逆操作:为Cancel阶段预留回滚路径
- 资源预留:确保后续阶段可完成
Confirm 阶段:业务确认
- 典型操作:
- 订单服务:将订单状态改为"已确认"
- 库存服务:将"预占"库存标记为"已售出"
- 支付服务:将冻结金额实际扣除
- 关键特性:
- 必须保证最终成功
- 操作需幂等
- 通常不提供回滚接口
Cancel 阶段:业务取消
- 典型操作:
- 订单服务:将订单状态改为"已取消"
- 库存服务:将"预占"库存恢复为"可用"
- 支付服务:解冻用户资金
- 关键特性:
- 必须正确处理部分成功场景
- 操作需幂等
- 需处理资源过期等边界情况
异常处理机制
- 空回滚:Try未执行时收到Cancel指令
- 防悬挂:Cancel先于Try执行的情况
- 重试机制:网络抖动时的自动重试
- 事务状态查询:超时事务的最终状态确认
1.3 TCC 与其他分布式事务方案的对比
详细对比分析
| 方案 | 实现层级 | 一致性保证 | 性能影响 | 适用场景 | 典型实现框架 |
|---|---|---|---|---|---|
| TCC | 业务层 | 最终一致 | 低 | 高并发、业务逻辑可控 | Seata、Hmily |
| 2PC | 资源层 | 强一致 | 高 | 金融、账务等严格一致场景 | XA协议 |
| 本地消息表 | 应用层 | 最终一致 | 中 | 异步通知、数据同步 | RocketMQ事务消息 |
| SAGA | 业务层 | 最终一致 | 中 | 长事务、跨多服务流程 | ServiceComb Pack |
| Seata AT | 资源层 | 最终一致 | 中 | 简单事务、希望低侵入 | Seata |
选型建议
- 高并发场景:优先考虑TCC,如秒杀、电商下单
- 严格一致性:考虑2PC,如银行转账
- 简单异步任务:本地消息表即可满足
- 长业务流程:SAGA模式更合适
- 快速改造:Seata AT模式侵入性最低
TCC的适用边界
-
优势场景:
- 性能敏感型业务
- 需要柔性事务的业务
- 业务逻辑清晰的场景
-
局限场景:
- 业务无法拆分Try/Confirm/Cancel
- 不支持幂等操作的服务
- 第三方服务无法提供补偿接口
通过这种对比分析,开发者可以更清晰地根据业务特征选择最适合的分布式事务解决方案。
二、TCC 的三阶段流程深度拆解
以 "电商下单" 这一典型场景为例,详细拆解 TCC 的三阶段流程。假设一个下单流程涉及三个服务:订单服务、库存服务、支付服务,具体流程如下:
2.1 场景定义
用户购买一件商品,流程需满足:
- 订单服务创建订单(状态为"待支付")
- 库存服务扣减商品库存(确保有货)
- 支付服务扣减用户账户余额(完成支付)
- 事务一致性要求:
- 所有步骤成功后,订单状态更新为"已支付"
- 若任一步骤失败,回滚所有操作
典型异常场景举例:
- 用户购买商品A(库存仅剩1件),同时有另一个用户也在购买同一商品
- 用户账户余额不足(如账户余额100元,商品价格120元)
- 网络抖动导致服务间通信超时
2.2 Try 阶段:资源检查与预留
Try 阶段的核心目标是"检查资源是否可用,并预留资源",确保后续的 Confirm 或 Cancel 操作不会因为资源不足而失败。
各服务的 Try 操作详解:
订单服务:
- 检查订单参数合法性:
- 商品ID是否存在且有效
- 用户ID是否有效
- 购买数量是否为正整数
- 创建订单记录:
- 状态设为"待确认"(而非"待支付"),避免与最终状态混淆
- 订单有效期设为30分钟(超时自动取消)
- 预留订单号:
- 使用分布式ID生成器确保唯一性
- 记录订单创建日志(含操作时间、操作人)
库存服务:
- 库存检查:
- 查询商品当前库存(stock字段)
- 比较库存与购买数量(需考虑已预占库存pre_stock)
- 预占库存:
- 原子操作:
pre_stock = pre_stock + 购买数量 - 更新库存版本号(乐观锁机制)
- 原子操作:
- 记录操作日志:
- 包含操作ID、商品ID、预占数量、操作时间
- 日志状态标记为"预占中"
支付服务:
- 余额检查:
- 查询用户可用余额(balance - frozen_balance)
- 比较可用余额与订单金额
- 资金冻结:
- 原子操作:
frozen_balance = frozen_balance + 支付金额 - 更新账户版本号(乐观锁机制)
- 原子操作:
- 记录资金操作:
- 生成冻结流水号
- 记录冻结金额、时间、关联订单号
Try 阶段的关键要求:
-
幂等性设计:
- 通过唯一操作ID(如订单号+服务名)判断是否已执行
- 示例:库存服务收到重复Try请求时,先查询预占日志
-
资源预留策略:
- 预库存=实际购买数量(不允许超额预留)
- 冻结金额=订单实际金额(考虑优惠券等扣除)
-
业务隔离性:
- 预占资源需设置有效期(如30分钟)
- 通过定时任务清理超时未确认的预留
-
性能考量:
- Try操作应快速完成(通常<100ms)
- 避免长事务阻塞(设置合理超时时间)
2.3 Confirm 阶段:业务正式提交
只有当所有服务的 Try 操作都执行成功后,TCC 事务协调器才会触发 Confirm 阶段,正式提交业务逻辑。
各服务的 Confirm 操作详解:
订单服务:
- 状态更新:
- 原子操作:
status = '已支付' WHERE order_id = ? AND status = '待确认' - 更新订单支付时间
- 原子操作:
- 日志记录:
- 记录确认操作日志(含确认时间、操作人)
- 标记订单为"已完成"状态
库存服务:
- 库存扣减:
- 原子操作:
UPDATE inventory SET stock = stock - ?, pre_stock = pre_stock - ? WHERE item_id = ? AND version = ?
- 原子操作:
- 清理预占:
- 删除或标记预占日志为"已确认"
- 更新库存最后操作时间
支付服务:
- 实际扣款:
- 原子操作:
UPDATE account SET balance = balance - ?, frozen_balance = frozen_balance - ? WHERE user_id = ? AND version = ?
- 原子操作:
- 生成支付流水:
- 记录支付完成时间
- 生成支付凭证(可用于售后)
Confirm 阶段的关键要求:
-
幂等性保障:
- 通过状态机校验(如订单必须处于"待确认"状态)
- 示例:支付服务收到重复Confirm时,先查询支付流水
-
事务最终性:
- Confirm成功后不允许回滚(设计时需严格验证业务逻辑)
- 关键数据需持久化到可靠存储
-
性能优化:
- 避免在Confirm阶段执行复杂计算
- 可采用异步确认机制(需保证最终一致性)
-
异常处理:
- 对Confirm失败的情况需有告警机制
- 设计自动重试策略(如指数退避算法)
2.4 Cancel 阶段:业务回滚与资源释放
若任一服务的 Try 操作失败(如库存不足、用户余额不足),或业务流程因其他原因中断(如网络超时),TCC 事务协调器会触发 Cancel 阶段,释放 Try 阶段预留的资源。
各服务的 Cancel 操作详解:
订单服务:
- 状态回滚:
UPDATE orders SET status = '已取消' WHERE order_id = ? AND status = '待确认'- 记录取消原因(如"库存不足")
- 清理操作:
- 标记订单为"已失效"
- 记录取消操作日志
库存服务:
- 库存恢复:
- 原子操作:
UPDATE inventory SET pre_stock = pre_stock - ? WHERE item_id = ? AND version = ?
- 原子操作:
- 日志处理:
- 标记预占日志为"已取消"
- 记录库存恢复时间
支付服务:
- 资金解冻:
- 原子操作:
UPDATE account SET frozen_balance = frozen_balance - ? WHERE user_id = ? AND version = ?
- 原子操作:
- 生成解冻流水:
- 记录解冻操作时间
- 关联原始冻结记录
Cancel 阶段的关键要求:
-
幂等性设计:
- 通过操作日志判断是否已执行Cancel
- 示例:支付服务收到Cancel时先查询冻结记录状态
-
资源释放完整性:
- 必须释放所有Try阶段预留的资源
- 需要处理部分Cancel成功的情况
-
重试机制:
- 对Cancel失败需有自动重试策略
- 可设置最大重试次数(如5次)
-
监控告警:
- 对频繁Cancel需有业务告警
- 记录Cancel原因用于业务分析
2.5 TCC 的状态流转模型
结合上述流程,我们可以总结出 TCC 事务的状态流转模型:
状态流转图:
初始状态 → Try成功 → 待确认状态 → Confirm → 已确认状态
↘ Cancel → 初始状态
关键状态说明:
-
初始状态:
- 资源未被操作(如库存未预占、资金未冻结)
- 业务数据不存在或为初始值
-
待确认状态:
- 核心中间状态(通常持续秒级)
- 资源已被预留但未最终提交
- 需要设置超时机制(默认30分钟)
-
终态处理:
- 已确认状态:业务数据完整持久化
- 已取消状态:资源完全释放
状态监控机制:
-
定时补偿任务:
- 扫描长期处于"待确认"状态的事务
- 根据日志决定执行Confirm或Cancel
-
状态修复策略:
- 对不一致状态提供人工干预接口
- 记录状态修复日志用于审计
-
可视化监控:
- 展示各状态事务数量
- 统计各阶段耗时指标
三、TCC 的关键技术点与实践难点
TCC(Try-Confirm-Cancel)作为分布式事务解决方案,其实现看似简单,但在实际开发中会遇到诸多技术挑战。本节将深入讲解 TCC 的关键技术点,并提供详细的解决方案和实践建议。
3.1 幂等性设计:避免重复执行导致的数据不一致
问题描述:
由于网络超时、服务重试等原因,Try/Confirm/Cancel 操作可能被重复执行。若未做幂等处理,会导致数据错误(如重复扣减库存、重复扣款)。这种问题在微服务架构中尤为常见,特别是在网络抖动或服务短暂不可用的情况下。
解决方案:
1. 基于唯一标识的幂等控制
实现步骤:
- 为每个 TCC 事务生成唯一的 "事务 ID"(推荐使用 UUID 或雪花算法生成的分布式 ID)
- 在每个服务的操作日志表中设计字段:"事务 ID + 操作类型"(Try/Confirm/Cancel)
- 每次执行操作前,先查询日志表:
- 若已存在相同记录,直接返回成功(避免重复执行)
- 若不存在,执行操作并记录日志(确保后续相同请求可识别)
技术实现示例:
-- 操作日志表设计
CREATE TABLE tcc_operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tx_id VARCHAR(64) NOT NULL, -- 事务ID
service_name VARCHAR(64) NOT NULL, -- 服务名称
operation_type VARCHAR(16) NOT NULL, -- Try/Confirm/Cancel
status TINYINT NOT NULL, -- 执行状态
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tx_op (tx_id, operation_type)
);
2. 业务层幂等判断
具体业务场景处理:
- 库存服务:
- Confirm 操作:在扣减实际库存前,先检查预库存是否已扣减(通过
reserved_stock字段判断) - Cancel 操作:在恢复预库存前,先检查是否已预留库存(通过
reserved_stock>0判断)
- Confirm 操作:在扣减实际库存前,先检查预库存是否已扣减(通过
- 支付服务:
- Try 操作:冻结资金前检查账户状态和可用余额
- Cancel 操作:解冻资金前检查资金是否已冻结(通过
frozen_amount字段判断)
3.2 空回滚与悬挂问题:边界场景的处理
3.2.1 空回滚(Null Rollback)
问题描述:
某个服务的 Try 操作尚未执行(或执行失败),但协调器因超时触发了该服务的 Cancel 操作,导致 Cancel 操作 "无资源可回滚",即空回滚。
典型场景示例:
-
订单创建流程:
- 库存服务 Try 操作因网络超时未执行(未扣减预库存)
- 协调器认为 Try 操作失败,触发 Cancel 操作
- 此时 Cancel 操作尝试恢复预库存,但预库存并未变化
-
支付流程:
- 支付服务 Try 操作因数据库连接池满被拒绝
- 协调器未收到响应,超时后触发 Cancel
- Cancel 操作执行时发现资金未被冻结
解决方案:
实现逻辑:
- 在 Cancel 操作中先检查 "Try 操作是否已执行"
- 通过查询操作日志表:
- 若不存在该事务 ID 的 Try 记录:
- 记录空回滚日志(标记为
NULL_ROLLBACK) - 直接返回成功(视为空回滚处理)
- 记录空回滚日志(标记为
- 若存在 Try 记录:
- 执行正常的回滚逻辑
- 更新操作状态为
ROLLBACKED
- 若不存在该事务 ID 的 Try 记录:
代码示例:
public boolean cancel(String txId) {
// 1. 检查是否已执行过Try
if (!operationLogRepository.existsByTxIdAndOperationType(txId, "TRY")) {
// 记录空回滚
OperationLog log = new OperationLog(txId, "CANCEL", "NULL_ROLLBACK");
operationLogRepository.save(log);
return true;
}
// 2. 执行正常回滚逻辑
return doBusinessRollback(txId);
}
3.2.2 悬挂(Suspension)
问题描述:
某个服务的 Cancel 操作已执行(释放资源),但由于网络延迟,该服务的 Try 操作后续又成功执行,导致资源被重复预留,而 Cancel 操作已无法回滚,即悬挂。
典型场景示例:
-
库存服务场景:
- 协调器调用库存服务 Cancel 成功(释放预占库存)
- Try 请求因网络延迟在Cancel之后到达
- Try 操作成功执行(再次预占库存)
- 导致库存被双重占用
-
优惠券服务场景:
- 优惠券 Cancel 已执行(恢复优惠券状态)
- Try 操作延迟到达并成功锁定优惠券
- 导致优惠券被锁定但无后续处理
解决方案:
实现逻辑:
- 在 Try 操作中先检查 "Cancel 操作是否已执行"
- 通过查询操作日志表:
- 若存在该事务 ID 的 Cancel 记录:
- 记录悬挂警告日志
- 直接返回失败(阻止 Try 操作执行)
- 若不存在 Cancel 记录:
- 执行正常 Try 逻辑
- 记录操作日志
- 若存在该事务 ID 的 Cancel 记录:
代码示例:
public boolean try(String txId) {
// 1. 检查是否已执行过Cancel
if (operationLogRepository.existsByTxIdAndOperationType(txId, "CANCEL")) {
log.warn("悬挂事务 detected, txId: {}", txId);
return false;
}
// 2. 执行正常Try逻辑
return doBusinessTry(txId);
}
3.3 事务协调器:TCC 的 "大脑"
TCC 的三个阶段需要一个 "协调器" 来统一调度,其核心职责包括:
核心功能架构
-
事务生命周期管理:
- 生成全局唯一的 transactionId(XID)
- 维护事务状态机(包括:INIT、TRYING、TRY_FAILED、CONFIRMING、CONFIRMED、CANCELLING、CANCELLED)
- 提供事务状态查询接口
-
参与者调度:
- 并行/串行调用各参与者的Try接口
- 根据Try结果决定执行Confirm或Cancel
- 处理参与者调用超时、失败等情况
-
异常处理机制:
- 超时处理(配置合理的超时阈值)
- 失败重试(指数退避策略)
- 死锁检测与处理
-
日志持久化:
- 事务日志(全局事务状态)
- 参与者日志(各服务操作状态)
- 实现服务重启后状态恢复
关键技术实现
1. 状态管理
状态转换图:
INIT → TRYING → (TRY_FAILED → CANCELLING → CANCELLED)
↘ (TRY_SUCCEEDED → CONFIRMING → CONFIRMED)
状态存储设计:
CREATE TABLE tcc_global_transaction (
xid VARCHAR(128) PRIMARY KEY,
status VARCHAR(32) NOT NULL,
application_id VARCHAR(64) NOT NULL,
transaction_name VARCHAR(128) NOT NULL,
begin_time BIGINT NOT NULL,
timeout BIGINT NOT NULL,
retry_count INT DEFAULT 0,
last_retry_time BIGINT,
version INT NOT NULL
);
2. 重试机制
实现策略:
- 指数退避算法:第一次重试间隔1秒,第二次3秒,第三次5秒...
- 最大重试次数限制(如5次)
- 失败告警机制(达到最大重试后触发告警)
代码示例:
public void retry(Transaction transaction) {
int retryCount = transaction.getRetryCount();
if (retryCount >= maxRetryCount) {
alertService.sendAlert(transaction);
return;
}
long delay = calculateDelay(retryCount);
scheduler.schedule(() -> {
// 执行重试逻辑
doRetry(transaction);
}, delay, TimeUnit.SECONDS);
}
private long calculateDelay(int retryCount) {
return (long) Math.pow(2, retryCount) - 1;
}
3. 日志持久化
设计要点:
- 事务日志分片存储(避免单表过大)
- 定期归档已完成事务
- 支持多存储后端(MySQL、MongoDB、Elasticsearch等)
主流框架对比
| 框架 | 特点 | 适用场景 |
|---|---|---|
| Seata TCC | 阿里开源,支持AT/TCC模式,集成Spring Cloud/Dubbo | 通用微服务架构 |
| Hmily | 轻量级,注解驱动,支持多种RPC框架 | Spring Cloud项目 |
| ByteTCC | 基于Spring Cloud,支持JTA/XA | 需要与XA事务集成 |
| TCC-transaction | 蚂蚁金服开源,高性能设计 | 金融级应用 |
3.4 数据一致性保障:最终一致性的实现
实现机制
1. 定时任务重试
设计要点:
- 分片扫描:避免集中扫描导致数据库压力
- 并行处理:提高重试效率
- 状态过滤:只处理特定状态的事务(如超过5分钟未确认)
实现示例:
@Scheduled(fixedDelay = 60000)
public void retryTimeoutTransactions() {
// 分片查询
List<Transaction> timeoutTransactions = transactionRepository
.findByStatusAndCreateTimeBefore(
TransactionStatus.CONFIRMING,
LocalDateTime.now().minusMinutes(5)
);
timeoutTransactions.forEach(tx -> {
// 异步重试
retryExecutor.execute(() -> retryConfirm(tx));
});
}
2. 补偿机制
实现方案:
- 人工补偿控制台:可视化界面展示异常事务
- 自动补偿规则:预设补偿策略(如自动重试3次后报警)
- 补偿日志审计:记录所有补偿操作
3. 数据校验
校验策略:
- 定时全量校验(凌晨低峰期执行)
- 实时增量校验(通过消息队列触发)
- 校验规则引擎(支持自定义规则)
典型校验场景:
-
订单-库存校验:
-- 查找不一致数据 SELECT o.order_id, o.product_id, o.quantity, i.stock FROM orders o LEFT JOIN inventory i ON o.product_id = i.product_id WHERE o.status = 'PAID' AND i.stock < 0; -
支付-账户校验:
-- 检查冻结金额与实际扣款 SELECT t.tx_id, a.frozen_amount, t.amount FROM transactions t JOIN accounts a ON t.account_id = a.account_id WHERE t.status = 'CONFIRMED' AND a.frozen_amount > 0;
监控与告警
关键指标:
- 事务成功率(Try/Confirm/Cancel)
- 平均处理时长
- 重试次数分布
- 悬挂事务数量
实现方案:
- Prometheus + Grafana 监控
- ELK 日志分析
- 企业微信/钉钉告警
四、TCC 实践案例:基于 Seata 实现电商下单
4.1 环境准备
- 框架:Spring Boot 2.7.x、Spring Cloud Alibaba 2021.0.4.0
- 分布式事务框架:Seata 1.6.1
- 数据库:MySQL 8.0(订单库、库存库、支付库)
- 注册中心:Nacos 2.2.0
4.2 核心代码实现
4.2.1 定义 TCC 接口(API 层)
订单服务 TCC 接口
/**
* 订单服务TCC接口
* 使用Seata的@TwoPhaseBusinessAction注解定义TCC事务
* 包含Try、Confirm、Cancel三个核心方法
*/
public interface OrderTccService {
/**
* Try阶段:创建待确认订单
* @param orderId 订单ID(业务唯一标识)
* @param userId 用户ID
* @param productId 商品ID
* @param quantity 购买数量
* @param amount 订单金额
*/
@TwoPhaseBusinessAction(
name = "orderTcc",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
void tryCreateOrder(
@BusinessActionContextParameter(paramName = "orderId") String orderId,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
/**
* Confirm阶段:确认订单(更新状态为已支付)
* @param context 事务上下文,包含事务ID和业务参数
*/
void confirm(BusinessActionContext context);
/**
* Cancel阶段:取消订单(更新状态为已取消)
* @param context 事务上下文,包含事务ID和业务参数
*/
void cancel(BusinessActionContext context);
}
库存服务 TCC 接口
/**
* 库存服务TCC接口
* 处理商品库存的扣减和恢复
*/
public interface StockTccService {
/**
* Try阶段:预扣减库存
* @param transactionId 全局事务ID
* @param productId 商品ID
* @param quantity 扣减数量
*/
@TwoPhaseBusinessAction(
name = "stockTcc",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
void tryDeductStock(
@BusinessActionContextParameter(paramName = "transactionId") String transactionId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity
);
void confirm(BusinessActionContext context);
void cancel(BusinessActionContext context);
}
支付服务 TCC 接口
/**
* 支付服务TCC接口
* 处理用户余额的冻结和解冻
*/
public interface PaymentTccService {
/**
* Try阶段:冻结用户余额
* @param transactionId 全局事务ID
* @param userId 用户ID
* @param amount 冻结金额
*/
@TwoPhaseBusinessAction(
name = "paymentTcc",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
void tryFreezeBalance(
@BusinessActionContextParameter(paramName = "transactionId") String transactionId,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
void confirm(BusinessActionContext context);
void cancel(BusinessActionContext context);
}
4.2.2 实现 TCC 接口(服务层)
库存服务实现示例
/**
* 库存服务TCC实现类
* 实现预扣库存、确认扣减和恢复库存的逻辑
*/
@Service
public class StockTccServiceImpl implements StockTccService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockOperateLogMapper logMapper;
@Override
public void tryDeductStock(String transactionId, Long productId, Integer quantity) {
// 1. 幂等性检查
StockOperateLog log = logMapper.selectByTransIdAndType(transactionId, "TRY");
if (log != null) {
return; // 已执行过Try操作
}
// 2. 检查库存可用性
Stock stock = stockMapper.selectByProductId(productId);
if (stock == null || stock.getAvailableStock() < quantity) {
throw new BusinessException("商品库存不足");
}
// 3. 执行预扣库存(冻结库存)
stockMapper.freezeStock(productId, quantity);
// 4. 记录操作日志
StockOperateLog operateLog = new StockOperateLog()
.setTransactionId(transactionId)
.setProductId(productId)
.setQuantity(quantity)
.setOperateType("TRY")
.setOperateTime(new Date());
logMapper.insert(operateLog);
}
@Override
public void confirm(BusinessActionContext context) {
String transactionId = context.getXid();
Long productId = Long.valueOf(context.getActionContext("productId").toString());
Integer quantity = Integer.valueOf(context.getActionContext("quantity").toString());
// 1. 幂等性检查
if (logMapper.selectByTransIdAndType(transactionId, "CONFIRM") != null) {
return;
}
// 2. 确认扣减库存(实际扣减)
stockMapper.confirmDeduction(productId, quantity);
// 3. 记录确认日志
StockOperateLog log = new StockOperateLog()
.setTransactionId(transactionId)
.setProductId(productId)
.setQuantity(quantity)
.setOperateType("CONFIRM")
.setOperateTime(new Date());
logMapper.insert(log);
}
@Override
public void cancel(BusinessActionContext context) {
String transactionId = context.getXid();
Long productId = Long.valueOf(context.getActionContext("productId").toString());
Integer quantity = Integer.valueOf(context.getActionContext("quantity").toString());
// 1. 幂等性检查
if (logMapper.selectByTransIdAndType(transactionId, "CANCEL") != null) {
return;
}
// 2. 处理空回滚情况
StockOperateLog tryLog = logMapper.selectByTransIdAndType(transactionId, "TRY");
if (tryLog == null) {
// 记录空回滚日志
StockOperateLog log = new StockOperateLog()
.setTransactionId(transactionId)
.setProductId(productId)
.setQuantity(quantity)
.setOperateType("CANCEL")
.setOperateTime(new Date())
.setRemark("空回滚");
logMapper.insert(log);
return;
}
// 3. 执行库存恢复
stockMapper.restoreStock(productId, quantity);
// 4. 记录取消日志
StockOperateLog log = new StockOperateLog()
.setTransactionId(transactionId)
.setProductId(productId)
.setQuantity(quantity)
.setOperateType("CANCEL")
.setOperateTime(new Date());
logMapper.insert(log);
}
}
关键实现说明:
- 幂等性处理:每个阶段都通过事务日志表检查是否已执行过相同操作
- 空回滚处理:Cancel阶段检查Try操作是否执行,避免无效操作
- 资源预留:Try阶段只冻结资源不实际扣减
- 状态转换:
- Try:创建临时状态(如"预扣库存")
- Confirm:转为最终状态(如"已扣减")
- Cancel:恢复原始状态
4.2.3 事务发起者(下单服务)
@Service
public class OrderServiceImpl implements OrderService {
// 注入TCC服务组件
@Autowired
private OrderTccService orderTccService;
@Autowired
private StockTccService stockTccService;
@Autowired
private PaymentTccService paymentTccService;
// Seata全局事务扫描器
@Autowired
private GlobalTransactionScanner globalTransactionScanner;
/**
* 分布式事务下单流程
* @param userId 用户ID
* @param productId 商品ID
* @param quantity 购买数量
* @param amount 订单金额
* @return 订单编号
* @throws BusinessException 业务异常
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class) // Seata全局事务注解
public String createOrder(Long userId, Long productId, Integer quantity, BigDecimal amount) {
// 1. 生成全局事务ID和订单ID
String transactionId = GlobalTransactionContext.getCurrentOrCreate().getXid();
String orderId = "ORD" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
try {
// 2. 调用各服务的Try阶段(顺序可根据业务调整)
// 2.1 订单服务Try:创建待确认订单
orderTccService.tryCreateOrder(orderId, userId, productId, quantity, amount);
// 2.2 库存服务Try:预扣库存
stockTccService.tryDeductStock(transactionId, productId, quantity);
// 2.3 支付服务Try:冻结余额
paymentTccService.tryFreezeBalance(transactionId, userId, amount);
// 3. 若所有Try操作成功,Seata会自动触发Confirm阶段
return orderId;
} catch (Exception e) {
// 4. 若任一Try操作失败,Seata会自动触发Cancel阶段
log.error("下单事务失败,事务ID:{},错误:{}", transactionId, e.getMessage());
throw new BusinessException("下单失败:" + e.getMessage());
}
}
}
4.3 数据库表设计
4.3.1 订单表(order_info)
CREATE TABLE `order_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` varchar(64) NOT NULL COMMENT '订单编号(业务唯一)',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`quantity` int NOT NULL COMMENT '购买数量',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` varchar(20) NOT NULL DEFAULT 'PENDING_CONFIRM' COMMENT '订单状态:
- PENDING_CONFIRM(待确认)
- PAID(已支付)
- CANCELLED(已取消)',
`transaction_id` varchar(128) DEFAULT NULL COMMENT '全局事务ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
4.3.2 库存表(stock_info)
CREATE TABLE `stock_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`stock` int NOT NULL DEFAULT '0' COMMENT '实际可用库存',
`frozen_stock` int NOT NULL DEFAULT '0' COMMENT '冻结库存(Try阶段预扣)',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id` (`product_id`),
KEY `idx_stock` (`stock`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品库存表';
4.3.3 TCC操作日志表(stock_tcc_log)
CREATE TABLE `stock_tcc_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`transaction_id` varchar(128) NOT NULL COMMENT '全局事务XID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`quantity` int NOT NULL COMMENT '操作数量(正数扣减/负数回补)',
`status` tinyint NOT NULL COMMENT '操作状态:
1-TRYING
2-CONFIRMED
3-CANCELLED',
`action_type` varchar(20) NOT NULL COMMENT '操作类型:
- TRY
- CONFIRM
- CANCEL
- COMPENSATE',
`retry_count` int DEFAULT '0' COMMENT '重试次数',
`next_retry_time` datetime DEFAULT NULL COMMENT '下次重试时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tx_product` (`transaction_id`,`product_id`),
KEY `idx_status_retry` (`status`,`next_retry_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存TCC操作日志表';
4.4 测试场景验证
4.4.1 正常流程测试
-
测试步骤:
- 调用
createOrder接口,传入参数:{ "userId": 10001, "productId": 2001, "quantity": 2, "amount": 199.98 } - 观察数据库变更:
- 订单表新增记录,状态为
PENDING_CONFIRM - 库存表的
frozen_stock增加2 - 支付冻结金额增加199.98
- 订单表新增记录,状态为
- 等待3秒后检查:
- 订单状态变为
PAID - 库存表的
stock减少2,frozen_stock归零 - 支付金额完成扣款
- 订单状态变为
- 调用
-
日志验证:
- 在
stock_tcc_log中会看到完整的事务日志:TRY -> CONFIRM 完整链路
- 在
4.4.2 异常场景测试:库存不足
-
模拟场景:
- 设置商品2001的库存为1
- 请求购买数量2
-
执行过程:
- 订单服务Try成功(创建待确认订单)
- 库存服务Try时抛出
StockNotEnoughException - Seata触发全局回滚
-
结果验证:
- 订单表:
- 状态变为
CANCELLED - 更新时间为回滚时间
- 状态变为
- 库存表:
stock和frozen_stock无变化
- 日志表:
- 订单服务有
CANCEL记录 - 库存服务有
空回滚标记
- 订单服务有
- 订单表:
-
特殊处理:
- 对于未执行Try的服务,Cancel操作会记录
"NO_TRY_RECORD"备注 - 通过
transaction_id可以追踪完整事务链路
- 对于未执行Try的服务,Cancel操作会记录
五、TCC 的监控与运维:确保事务稳定运行
TCC 事务的稳定性依赖于完善的监控和运维体系,尤其是在高并发场景下,需实时关注事务状态、异常情况,并及时处理故障。良好的监控运维能够提前发现问题,快速定位故障点,最大限度减少业务损失。
5.1 关键监控指标
事务成功率
- 统计方法:统计全局事务中"已确认"状态的比例 = (成功完成的事务数/发起的事务总数)×100%
- 监控阈值:建议低于99.9%触发告警
- 业务影响:成功率下降可能反映系统存在稳定性问题或业务逻辑缺陷
事务超时率
- 监控对象:统计"待确认""待取消"状态超过阈值时间(典型值30秒)的事务比例
- 风险分析:超时事务可能导致数据库连接占用、资源锁定等问题
- 优化方向:可结合业务场景调整超时阈值,如金融支付可设为10秒,电商交易可设为60秒
重试次数分布
- 监控维度:Confirm/Cancel操作的重试次数分布
- 告警策略:单事务重试超过5次触发告警
- 典型案例:某电商平台发现重试次数过多时,通常是由于下游服务响应超时或网络抖动
异常类型分布
- 主要异常类型:
- 空回滚:Try未执行却收到Cancel
- 悬挂:Try超时后Cancel才到达
- 资源不足:预占资源不足
- 分析方法:建立异常类型看板,统计各类型占比
- 优化示例:某银行系统发现空回滚占比高,经排查是前端重复提交导致
资源占用情况
- 监控对象:预库存、冻结资金等预留资源
- 安全阈值:建议不超过总库存的20%
- 处理机制:超过阈值时自动触发资源扫描和释放任务
5.2 监控工具与实现方案
基于Prometheus + Grafana的实时监控
- 埋点实现:
- 在TCC协调器代码中插入指标采集点
- 通过/metrics接口暴露事务状态、耗时等指标
- 采集配置:
# Prometheus配置示例 scrape_configs: - job_name: 'tcc-coordinator' static_configs: - targets: ['coordinator:8080'] - 仪表盘设计:
- 核心指标:事务成功率、TPS、平均耗时
- 异常指标:重试率、超时率、错误码分布
- 资源监控:数据库连接数、线程池使用率
事务日志可视化平台
- 日志收集架构:
业务服务 → Logstash → Elasticsearch → Kibana ↑ Filebeat - 关键字段索引:
- 事务ID(txId)
- 服务名称(service)
- 操作类型(action: TRY/CONFIRM/CANCEL)
- 时间戳(timestamp)
- 执行状态(status)
- 查询示例:
GET /tcc-logs/_search { "query": { "bool": { "must": [ {"match": {"txId": "12345"}}, {"range": {"timestamp": {"gte": "now-1h"}}} ] } } }
5.3 故障处理流程
事务卡住处理流程
- 诊断步骤:
- 查询事务状态表:
SELECT * FROM tcc_transaction WHERE status='PENDING' AND create_time < NOW()-30 - 检查关联服务日志:
grep 'txId=12345' /var/log/service/*.log
- 查询事务状态表:
- 处理方案:
- 服务恢复场景:调用协调器重试API
curl -X POST http://coordinator/api/retry -d '{"txId":"12345"}' - 代码缺陷场景:
- 修复业务逻辑
- 通过管理界面强制完成事务
- 执行数据补偿脚本
- 服务恢复场景:调用协调器重试API
数据不一致处理
- 校验工具设计:
-- 订单与库存校验SQL示例 SELECT o.order_id FROM orders o LEFT JOIN inventory i ON o.item_id = i.item_id WHERE o.status='PAID' AND i.actual_stock > 0 AND NOT EXISTS ( SELECT 1 FROM inventory_log WHERE order_id=o.order_id AND operation='CONFIRM' ) - 补偿执行流程:
- 校验异常记录加入补偿队列
- 补偿服务顺序执行Confirm操作
- 记录补偿日志防止重复执行
资源泄漏处理
- 泄漏检测SQL:
-- 预库存泄漏检测 SELECT item_id, SUM(frozen_qty) FROM inventory WHERE tx_status='CANCELED' GROUP BY item_id HAVING SUM(frozen_qty) < 0; - 批量修复脚本:
-- 预库存恢复脚本 UPDATE inventory SET frozen_qty = frozen_qty + ABS(leak_amount) WHERE item_id IN ( SELECT item_id FROM temp_leak_items ); - 执行注意事项:
- 先在测试环境验证脚本
- 分批执行(如每次100条)
- 执行前后记录库存快照
- 添加事务回滚机制
六、TCC 的性能优化:在一致性与性能间找平衡
TCC(Try-Confirm-Cancel)作为一种分布式事务解决方案,其核心优势在于高性能和灵活性。但在实际生产环境中,若设计不当,可能导致严重的性能瓶颈,如数据库锁竞争、重试风暴等问题。本节将深入探讨针对性的优化方案,帮助开发者在保证事务一致性的前提下最大化系统性能。
6.1 减少数据库锁竞争
资源预留粒度优化
详细实现方案:
- 采用分片式库存设计,将传统的一个商品一行库存记录改为多行分片记录
- 分片策略可采用:哈希分片(根据商品ID哈希)、范围分片(按库存量划分)或轮询分片
- 每个分片设置独立的预扣减量字段和状态字段
进阶优化:
- 分片预热:在高峰时段前预先加载分片信息到缓存
- 智能分片选择:根据历史数据动态调整分片大小
- 分片动态扩容:当单个分片压力过大时自动拆分
典型应用场景:
- 电商秒杀活动:将热门商品库存分为100个分片,每个用户请求随机路由到一个分片
- 票务系统:将演出票按区域和座位等级进行分片管理
读写分离
实现细节:
- 在Try阶段配置专门的只读数据源
- 使用数据库中间件(如ShardingSphere)自动路由查询请求
- 主从同步延迟监控:当延迟超过阈值时自动切换路由策略
注意事项:
- 从库读取需要设置合理的超时时间
- 对实时性要求极高的查询仍需路由到主库
- 建议采用半同步复制确保数据一致性
批量处理
批量事务处理流程:
- 请求收集:将短时间内的多个事务请求暂存到缓冲队列
- 批次划分:按固定大小(如100个)或时间窗口(如50ms)划分批次
- 合并执行:将同类型操作合并为批量SQL执行
- 结果分发:将执行结果分别返回给各请求方
性能对比数据:
| 处理方式 | TPS | 平均响应时间 | 数据库负载 |
|---|---|---|---|
| 单条处理 | 1500 | 120ms | 75% |
| 批量处理 | 4500 | 65ms | 40% |
6.2 优化重试机制
动态重试间隔
智能重试算法设计:
public long calculateRetryInterval(int retryCount, Exception cause) {
if (cause instanceof NetworkException) {
return Math.min(1000 * (1 << retryCount), 10000); // 指数退避,上限10秒
} else if (cause instanceof BusinessException) {
return 5000; // 业务异常固定5秒间隔
}
return 3000; // 默认3秒
}
重试策略配置建议:
- 网络类异常:采用指数退避策略(1s, 2s, 4s...)
- 资源竞争异常:采用随机抖动间隔(2s±0.5s)
- 业务规则异常:采用固定长间隔(5s以上)
失败快速降级
降级处理流程:
- 异常分类:识别不可恢复异常(如账户冻结、商品下架)
- 熔断机制:当连续失败达到阈值时触发熔断
- 状态标记:将事务标记为"最终失败"状态
- 补偿通知:触发告警系统并生成对账任务
典型不可恢复异常清单:
- 账户状态异常(冻结、注销)
- 商品无效(下架、不存在)
- 业务规则冲突(限购、地域限制)
6.3 异步化处理非核心流程
Confirm操作异步化
核心/非核心操作划分标准:
- 核心操作:直接影响主业务正确性的操作(资金扣减、库存变更)
- 非核心操作:可延迟或重试的操作(通知、日志、衍生数据)
异步确认架构设计:
[主流程] → [核心确认] → [返回响应]
↓
[消息队列] → [异步消费者] → [非核心确认]
保证最终一致性的措施:
- 消息持久化存储
- 消费者幂等处理
- 死信队列监控
Cancel操作后台化
后台取消系统设计要点:
- 任务队列:采用可靠消息队列(如RocketMQ)
- 任务优先级:根据业务重要性设置多级队列
- 任务分片:大流量时自动分片处理
- 进度监控:实时展示待取消任务数量和处理速度
性能优化前后的对比:
| 指标 | 同步取消 | 异步取消 |
|---|---|---|
| 峰值处理能力 | 500 TPS | 5000 TPS |
| 平均延迟 | 200ms | 50ms |
| 资源占用率 | 80% | 30% |
注意事项:
- 需要确保Cancel操作最终一定会被执行
- 重要业务需要设置取消超时告警
- 系统重启后需要恢复未完成的任务
1459

被折叠的 条评论
为什么被折叠?



