分布式事务TCC模式详解:从原理到Seata实战与避坑指南 1. 项目概述为什么我们需要TCC在聊TCC之前我想先说说我踩过的一个坑。几年前我们系统里有个“下单减库存”的逻辑听着很简单对吧用户下单调用订单服务创建订单同时调用库存服务扣减库存。一开始服务都部署在一台机器上用个数据库事务就搞定了要么一起成功要么一起失败天下太平。后来业务量上来了不得不做微服务拆分订单和库存成了两个独立部署、各自拥有独立数据库的服务。问题就来了订单服务的事务提交了但调用库存服务时网络超时了库存没扣成结果就是订单生成了但商品没卖出去直接导致超卖。这就是典型的分布式事务问题——在跨服务、跨数据库的场景下如何保证多个操作要么全部成功要么全部回滚保持数据最终一致性。这时候你可能会想到两阶段提交2PC它确实是个标准协议。但2PC依赖于一个全局的事务协调者在准备阶段会锁定所有资源在分布式环境下这会导致严重的性能瓶颈和长时间的资源锁定可用性很差。对于高并发的互联网业务这几乎是不可接受的。于是一种更柔性、更适合互联网架构的解决方案——TCCTry-Confirm-Cancel模式就成为了我们的首选。简单来说TCC把一个大事务拆分成两个阶段但它的核心思想是“业务补偿”而不是“资源锁定”。它要求我们作为开发者对每一个参与事务的服务都需要实现三个业务逻辑Try尝试、Confirm确认、Cancel取消。这相当于把事务的控制权从数据库层面上提到了业务代码层面虽然增加了开发复杂度但换来了更高的性能和可用性。今天我就结合自己趟过的坑把TCC从设计思想到落地实操再到避坑指南给你彻底讲透。2. TCC模式的核心设计思想与原理拆解2.1 两阶段提交的业务化改造TCC本质上是对传统两阶段提交2PC的一种业务改造。在2PC中协调者询问参与者“能否提交”参与者回答“是”或“否”这个过程对业务是透明的。TCC则将这个“询问”阶段具象化为一个可执行的Try操作。Try阶段这个阶段的目标是“预留资源”。它不是直接执行业务操作而是完成所有业务的检查并预留好必要的资源确保后续的Confirm一定能成功。你可以把它理解成“下单预占库存”。在电商场景中Try操作不是直接扣减数据库里的真实库存而是在另一个地方比如一个“预占库存”表冻结这部分库存。同时订单服务也不是直接生成最终订单而是生成一个状态为“待确认”的订单。这个阶段完成后业务资源其实处于一个“中间状态”既没有被最终消耗也已经被锁定无法被其他事务使用。Confirm阶段当所有参与者的Try操作都成功返回后事务协调者就会发起Confirm指令。这个阶段才是真正的“提交”它需要利用Try阶段预留的资源去执行业务的最终操作。因为Try阶段已经确保了所有条件都满足所以Confirm操作必须保证成功通常设计成幂等的。接上例Confirm操作就是将“预占库存”真正扣除并将订单状态从“待确认”更新为“已支付”。Cancel阶段如果任何一个参与者的Try操作失败或者事务协调者决定回滚就会进入Cancel阶段。Cancel操作是对Try阶段预留资源的“释放”或“补偿”。它必须能将系统状态回滚到事务开始之前同样也需要保证幂等性。例如释放预占的库存将“待确认”的订单置为“已取消”。这个设计的精妙之处在于它将一个长时间的“资源锁定”过程缩短为一个快速的“资源预留”Try过程。Confirm和Cancel的执行速度通常很快这大大减少了对关键资源的占用时间提升了系统的并发能力。2.2 TCC与其它分布式事务方案的对比分布式事务的解决方案不止TCC一种理解它们的差异能帮你更好地做技术选型。这里我列一个简单的对比表方案核心思想一致性性能/可用性适用场景开发复杂度2PC/XA数据库层支持由事务管理器协调强一致性低同步阻塞资源锁定长传统单体应用数据库内部低数据库驱动TCC业务补偿两阶段提交的业务实现最终一致性高资源锁定时间短高并发、短流程、对一致性要求较高的业务高需实现三个接口本地消息表基于消息队列的最终一致性最终一致性高异步跨系统、执行时间较长的业务如支付通知中需维护消息状态最大努力通知定期重试不保证绝对成功弱一致性高对一致性要求不高的辅助业务如发送短信、记录日志低注意这里提到的“最大努力通知”是一种务实的妥协方案。比如支付成功后通知物流系统发货我们会尽最大努力多次重试去通知但即使最终通知失败也不会回滚核心的支付事务。它适用于允许一定数据延迟或最终可通过对账修复的场景。TCC方案在“一致性”和“性能”之间取得了较好的平衡。它不像2PC那样追求强一致性而牺牲可用性也不像最大努力通知那样过于宽松。它通过业务代码保证了数据的最终正确性是很多金融、交易类核心场景的首选。2.3 TCC事务的三大核心特性要实现一个健壮的TCC事务你必须保证三个核心特性这是很多新手容易忽略的地方空回滚Empty Rollback 这是指在没有调用Try方法的情况下直接调用了Cancel方法。怎么发生的比如Try请求发送到服务B时网络拥堵协调者超时认为失败于是发起了Cancel。但过了一会儿那个被堵在路上的Try请求终于到了服务B并执行成功了。此时服务B已经预留了资源却收到了一个Cancel指令。如果Cancel不做判断直接执行释放就会导致数据不一致资源被错误释放。正确的做法是在Cancel逻辑里先检查是否有对应的Try执行记录。如果没有说明是空回滚那么Cancel应该直接返回成功并插入一条已回滚的记录以防后续迟到的Try请求再被处理。防悬挂Anti-Suspension 这个场景和空回滚恰好相反。同样是网络问题Try请求晚到了但它在Cancel指令之后才到达参与者。如果此时Try正常执行并预留了资源那么这部分资源就永远被悬挂锁定住了因为Confirm永远不会再发生事务已回滚。解决方案是在Try逻辑里也要先检查。如果发现当前事务ID已经存在Cancel的执行记录表明事务已回滚那么这个迟到的Try就应该被丢弃不再执行业务。幂等性Idempotency 这是分布式系统设计的黄金法则在TCC中尤为重要。网络的不确定性可能导致协调者重复发送Confirm或Cancel指令。参与者必须保证无论收到多少次相同的指令执行的结果都是一样的。实现方式通常是通过一个“事务状态表”记录每个事务IDXID的最终状态Confirming/Confirmed, Cancelling/Cancelled。在执行Confirm/Cancel前先查状态如果已经是终态则直接返回成功。这三个特性是TCC模式能正确运行的基石在实现每一个TCC服务接口时都必须将它们考虑进去缺一不可。3. 一个电商下单场景的TCC实战拆解光讲理论太枯燥我们以一个最经典的“电商下单支付”场景为例把TCC的每一步都具象化。假设这个事务涉及三个服务订单服务Order、库存服务Stock和账户服务Account。3.1 第一阶段Try尝试/预留资源事务协调者可以是独立的组件也可以是嵌入在订单服务中的逻辑开始一个全局事务生成一个全局唯一的XID事务ID然后依次调用三个服务的Try接口。订单服务 Try操作在订单表中插入一条记录。关键字段包括order_id订单号user_idtotal_amountstatus此时设为TRY表示尝试中xid全局事务ID。资源预留这里“预留”的资源就是一个“订单创建”的可能性。通过将状态设为中间态防止其他系统如物流误认为订单已生效。同时订单ID应在此阶段生成后续阶段都基于此ID操作。SQL示例伪代码:INSERT INTO order (order_id, user_id, total_amount, status, xid) VALUES (#{orderId}, #{userId}, #{amount}, TRY, #{xid});库存服务 Try操作检查商品库存是否充足。如果充足则不直接扣减product.stock而是在一个单独的“库存预占表”中插入一条记录冻结这部分库存。资源预留预留的是具体的商品库存数量。SQL示例:-- 1. 检查真实库存 (乐观锁防止超卖) SELECT stock FROM product WHERE product_id #{productId} FOR UPDATE; -- 假设检查通过stock #{count} -- 2. 冻结库存预留资源 INSERT INTO stock_freeze (freeze_id, product_id, order_id, freeze_count, xid, state) VALUES (#{freezeId}, #{productId}, #{orderId}, #{count}, #{xid}, TRY); -- 3. 扣减可用库存可选也可以在Confirm做 UPDATE product SET stock stock - #{count} WHERE product_id #{productId};实操心得这里有个设计选择点是在Try阶段就扣减真实库存还是在Confirm阶段扣我推荐在Try阶段就扣。因为“可用库存 总库存 - 冻结库存”在Try阶段扣减真实库存可以更直观地防止超卖其他事务查询到的就是实时可售库存。只要保证Cancel阶段能正确加回即可。账户服务 Try操作检查用户账户余额是否足够。如果足够则不直接扣款而是将支付金额从“余额”字段转移到“冻结金额”字段。资源预留预留的是用户的资金。SQL示例:-- 1. 检查并冻结余额原子操作 UPDATE account SET balance balance - #{amount}, freezed freezed #{amount} WHERE user_id #{userId} AND balance #{amount}; -- 检查影响行数如果为0则表示余额不足如果以上三个Try操作全部成功第一阶段完成。此时用户的资金被冻结商品库存被锁定订单处于“待确认”状态。整个系统为最终的提交做好了准备。3.2 第二阶段Confirm确认/提交当协调者确认所有Try成功便发起第二阶段的Confirm调用。这些调用必须是幂等的。订单服务 Confirm操作将订单状态从TRY更新为CONFIRMED或PAID。实现要点先根据XID查询订单状态如果已是CONFIRMED则直接返回成功幂等。SQL示例:UPDATE order SET status CONFIRMED WHERE xid #{xid} AND status TRY;库存服务 Confirm操作确认冻结的库存。这里有两种做法一是简单地将冻结记录状态改为CONFIRMED二是删除冻结记录因为真实库存已在Try阶段扣减。我倾向于第二种更简洁。SQL示例:-- 方案一更新状态 UPDATE stock_freeze SET state CONFIRMED WHERE xid #{xid} AND state TRY; -- 方案二删除记录需确保Cancel逻辑能处理记录不存在的情况 DELETE FROM stock_freeze WHERE xid #{xid} AND state TRY;账户服务 Confirm操作确认冻结的资金。将“冻结金额”清零这部分资金正式被扣除。或者如果Try阶段只是标记这里才真正扣款。SQL示例:UPDATE account SET freezed freezed - #{amount} WHERE user_id #{userId} AND xid #{xid}; -- 通常需要关联xid防止处理错事务Confirm阶段成功后整个分布式事务完成。用户扣款成功库存减少订单生效。3.3 第二阶段Cancel取消/回滚如果任何一个Try失败比如账户余额不足协调者就会发起Cancel调用顺序一般与Try相反资源释放的常见顺序。账户服务 Cancel操作解冻资金。将冻结金额加回可用余额。实现要点必须防空回滚先检查是否有该XID的Try记录比如检查freezed金额或单独的日志表。如果没有是空回滚则插入一条回滚记录后直接返回成功。SQL示例:-- 1. 防空回滚检查假设有事务日志表 SELECT COUNT(*) FROM tx_log WHERE xid #{xid} AND service account_try; -- 如果为0则执行空回滚逻辑INSERT INTO tx_log (xid, service, state) VALUES (#{xid}, account_try, CANCELED); RETURN; -- 2. 解冻资金幂等可多次执行 UPDATE account SET balance balance #{amount}, freezed freezed - #{amount} WHERE user_id #{userId} AND freezed #{amount}; -- 更新事务日志状态 UPDATE tx_log SET state CANCELED WHERE xid #{xid} AND service account_try;库存服务 Cancel操作释放冻结的库存。如果Try阶段扣减了真实库存这里需要加回同时删除或标记冻结记录。实现要点同样需要防空回滚和防悬挂。SQL示例:-- 1. 防悬挂检查如果Cancel记录已存在则拒绝后续TryTry逻辑里要做 -- 2. 释放库存 UPDATE product p INNER JOIN stock_freeze sf ON p.product_id sf.product_id SET p.stock p.stock sf.freeze_count WHERE sf.xid #{xid} AND sf.state TRY; -- 3. 更新冻结记录状态 UPDATE stock_freeze SET state CANCELED WHERE xid #{xid} AND state TRY;订单服务 Cancel操作将订单状态更新为CANCELED。SQL示例:UPDATE order SET status CANCELED WHERE xid #{xid} AND status TRY;至此Cancel阶段完成所有资源释放系统状态回滚到事务开始前。4. 基于Seata框架的TCC快速实现手动管理TCC的三个阶段、事务日志、重试、空回滚等逻辑非常繁琐且容易出错。在实际生产中我们通常会使用成熟的分布式事务框架比如Seata。Seata的TCC模式帮我们封装了事务协调、日志持久化、重试机制等通用逻辑我们只需要专注于实现try、confirm、cancel这三个业务方法。4.1 Seata TCC模式的工作原理Seata TCC模式中有三个核心角色TM (Transaction Manager) 事务管理器定义全局事务的边界负责开启、提交或回滚全局事务。通常就是发起分布式事务的那个业务方法加上GlobalTransactional注解。RM (Resource Manager) 资源管理器管理分支事务的资源在这里就是我们的各个微服务它们需要实现TCC接口。TC (Transaction Coordinator) 事务协调器Seata-Server独立部署负责维护全局事务和分支事务的状态驱动全局事务的提交或回滚。工作流程可以简化为TM订单服务在业务方法上标注GlobalTransactional该方法执行时Seata会向TC申请一个全局事务IDXID。TM调用订单服务的TCC Try接口Seata会拦截调用将订单服务注册为一个分支事务到TC。TM继续调用库存服务、账户服务的TCC Try接口同样注册为分支事务。如果所有Try成功TM通知TC提交全局事务。TC会异步调用所有分支事务的Confirm接口。如果任一Try失败TM通知TC回滚全局事务。TC会异步调用所有已成功Try的分支事务的Cancel接口。4.2 代码实现示例以库存服务为例首先你需要定义一个TCC业务接口。这个接口需要遵循Seata的规范。// StockService.java public interface StockService { /** * TCC的try方法预留库存资源 * param businessActionContext 上下文Seata自动注入用于传递XID等参数 * param productId 商品ID * param count 扣减数量 * return 是否成功 */ TwoPhaseBusinessAction(name deductStockTcc, commitMethod confirmDeduct, rollbackMethod cancelDeduct) boolean tryDeduct(BusinessActionContext businessActionContext, BusinessActionContextParameter(paramName productId) String productId, BusinessActionContextParameter(paramName count) Integer count); /** * 二阶段confirm方法确认扣减库存 * param context 业务上下文 * return 是否成功 */ boolean confirmDeduct(BusinessActionContext context); /** * 二阶段cancel方法取消扣减释放库存 * param context 业务上下文 * return 是否成功 */ boolean cancelDeduct(BusinessActionContext context); }然后实现这个接口。这里要特别注意空回滚、防悬挂和幂等性的处理。// StockServiceImpl.java Service public class StockServiceImpl implements StockService { Autowired private StockMapper stockMapper; Autowired private StockFreezeMapper freezeMapper; // 操作库存冻结表 Override Transactional(rollbackFor Exception.class) public boolean tryDeduct(BusinessActionContext context, String productId, Integer count) { // 1. 防悬挂检查如果已经存在该xid的Cancel记录则拒绝Try StockFreeze freezeRecord freezeMapper.selectByXid(context.getXid()); if (freezeRecord ! null CANCELED.equals(freezeRecord.getState())) { // 悬挂发生记录日志并返回true让事务继续但实际不做事 log.warn(防悬挂生效忽略迟到的Try请求。xid: {}, context.getXid()); return true; } // 2. 业务检查与资源预留 // 检查真实库存加行锁防止超卖 Stock stock stockMapper.selectForUpdate(productId); if (stock.getAvailable() count) { throw new RuntimeException(库存不足); } // 3. 扣减可用库存真实库存 stockMapper.reduceAvailable(productId, count); // 4. 记录冻结信息预留资源 StockFreeze newFreeze new StockFreeze(); newFreeze.setXid(context.getXid()); newFreeze.setProductId(productId); newFreeze.setFreezeCount(count); newFreeze.setState(TRY); freezeMapper.insert(newFreeze); // Seata会自动将XID、productId、count等参数存入上下文供二阶段使用 return true; } Override public boolean confirmDeduct(BusinessActionContext context) { // 1. 幂等性检查根据XID查询冻结记录状态 StockFreeze freezeRecord freezeMapper.selectByXid(context.getXid()); if (freezeRecord null) { // 记录不存在可能是空回滚后Try才到或者已处理过。按幂等处理返回成功 log.warn(Confirm: 未找到冻结记录按幂等处理。xid: {}, context.getXid()); return true; } if (CONFIRMED.equals(freezeRecord.getState())) { // 已确认幂等返回成功 return true; } // 2. 执行业务确认这里我们的业务逻辑在Try阶段已基本完成Confirm只需更新状态 // 例如将冻结记录状态改为CONFIRMED或者直接删除确保Cancel能处理记录不存在的情况 freezeMapper.updateState(context.getXid(), CONFIRMED); // 如果Try阶段未扣真实库存这里需要执行最终扣减 // stockMapper.reduceStock(freezeRecord.getProductId(), freezeRecord.getFreezeCount()); log.info(库存确认完成xid: {}, context.getXid()); return true; } Override Transactional(rollbackFor Exception.class) public boolean cancelDeduct(BusinessActionContext context) { // 1. 空回滚检查查询Try记录 StockFreeze freezeRecord freezeMapper.selectByXid(context.getXid()); if (freezeRecord null) { // 没有Try记录是空回滚 // 插入一条回滚记录防止悬挂 StockFreeze cancelRecord new StockFreeze(); cancelRecord.setXid(context.getXid()); // 从上下文中获取参数 String productId (String) context.getActionContext(productId); Integer count (Integer) context.getActionContext(count); cancelRecord.setProductId(productId); cancelRecord.setFreezeCount(count); cancelRecord.setState(CANCELED); // 标记为已回滚 freezeMapper.insert(cancelRecord); log.info(空回滚处理完成插入回滚记录。xid: {}, context.getXid()); return true; } // 2. 幂等性检查如果已经是CANCELED状态直接返回 if (CANCELED.equals(freezeRecord.getState())) { return true; } // 3. 执行业务回滚恢复真实库存 stockMapper.addAvailable(freezeRecord.getProductId(), freezeRecord.getFreezeCount()); // 4. 更新冻结记录状态为CANCELED freezeMapper.updateState(context.getXid(), CANCELED); log.info(库存回滚完成xid: {}, context.getXid()); return true; } }实操心得在Seata TCC中try方法上的Transactional是控制本地事务的它保证try方法内的数据库操作扣库存、插入冻结记录原子性。而全局事务的提交回滚是由Seata TC通过调用confirm/cancel方法来控制的。confirm和cancel方法本身也应该是幂等的并且通常也需要本地事务(Transactional)来保证其内部数据操作的原子性。4.3 事务发起方TM的配置在订单服务中你需要开启全局事务。// OrderServiceImpl.java Service public class OrderServiceImpl implements OrderService { Autowired private OrderTccService orderTccService; // 订单本身的TCC服务 Autowired private StockService stockService; // 库存TCC服务Feign客户端 Autowired private AccountService accountService; // 账户TCC服务Feign客户端 Override GlobalTransactional(name createOrderTcc, timeoutMills 60000, rollbackFor Exception.class) public Order createOrder(OrderRequest request) { // 1. 生成全局事务ID (Seata自动处理) // 2. 执行订单TCC Try orderTccService.tryCreateOrder(...); // 3. 执行库存TCC Try (通过Feign远程调用) Boolean stockResult stockServiceFeignClient.tryDeduct(request.getProductId(), request.getCount()); if (!stockResult) { throw new RuntimeException(库存预留失败); } // 4. 执行账户TCC Try Boolean accountResult accountServiceFeignClient.tryFreeze(request.getUserId(), request.getAmount()); if (!accountResult) { throw new RuntimeException(余额冻结失败); } // 如果所有Try成功GlobalTransactional 注解会保证在方法成功退出后Seata TC自动触发所有Confirm // 如果任何一步抛出异常Seata TC会自动触发所有已成功Try的Cancel // return order; } }通过Seata框架我们大大简化了TCC模式的管理复杂度可以将更多精力放在业务逻辑的实现上。5. TCC模式落地中的常见陷阱与最佳实践即使理解了原理用了框架在实际落地TCC时依然有很多细节坑等着你。下面是我总结的几个关键点和最佳实践。5.1 事务的隔离性与并发控制TCC的Try阶段预留了资源这就引出了一个核心问题在Try之后Confirm之前这部分“中间状态”的数据对其他事务是否可见这就是分布式事务的隔离性问题。问题场景用户A下单Try成功冻结了库存在Confirm之前用户B也来买同一件商品。系统检查真实库存已扣减发现充足于是也走了Try流程冻结了库存。如果两个事务都Confirm就会导致超卖。解决方案悲观锁在Try阶段检查并冻结资源时使用SELECT ... FOR UPDATE之类的行锁确保同一资源串行化处理。这是最直接的方式但影响并发度。状态判断在业务逻辑中引入状态字段。例如库存冻结记录带有order_id。用户B的Try逻辑在检查时不仅要看真实库存还要检查是否存在未确认stateTRY的冻结记录且不属于自己本次的订单。如果有则认为资源已被占用Try失败。使用Seata的全局锁Seata AT模式提供了全局锁机制但TCC模式默认不依赖它。你可以结合业务状态判断来实现更灵活的并发控制。我的建议对于库存、余额这类强一致性要求的资源在Try阶段使用**“检查扣减记录”**在一个数据库事务内完成并利用数据库行锁FOR UPDATE是相对稳妥的方案。虽然牺牲了一点并发但保证了绝对的正确性。对于像“优惠券”、“活动参与资格”等可以适当超卖或快速失效的资源可以尝试更乐观的并发策略。5.2 允许空回滚但要严防悬挂空回滚和防悬挂是TCC正确性的生命线必须在每个参与服务的cancel和try方法中严格实现。空回滚处理模板根据XID查询Try阶段是否执行过查业务流水表或冻结表。如果没执行过执行“空回滚”逻辑通常是在日志表插入一条状态为CANCELED的记录然后直接返回成功。切记不要执行实际的业务回滚操作防悬挂处理模板在Try方法开始先根据XID查询是否有状态为CANCELED的记录。如果有说明Cancel指令先于Try到达悬挂发生此时Try应该直接返回成功或抛出一个特殊异常让框架认为Try成功但不执行任何实际的资源预留操作。同时记录日志告警。这两个机制共同作用确保了无论网络如何乱序最终状态都是一致的。5.3 Confirm/Cancel的幂等性与重试机制网络抖动、服务重启都可能导致TC重复调用Confirm或Cancel。因此这两个方法必须是幂等的。实现幂等的通用模式建立一张分支事务状态表记录XID、业务主键、状态TRYING/CONFIRMED/CANCELED。在Confirm/Cancel执行前先查此表。如果状态已是终态CONFIRMED或CANCELED直接返回成功。否则执行业务逻辑并在同一个本地事务中更新状态为终态。Seata框架本身会负责重试但前提是你的接口是幂等的。如果因为非幂等导致重试失败事务会一直处于未完成状态需要人工介入处理。5.4 业务设计上的取舍与妥协TCC并非银弹它要求你将业务逻辑拆分为Try/Confirm/Cancel这对某些业务来说是天然的对另一些则可能是灾难。适合TCC的业务具有明显“预留”概念的场景。如库存冻结、余额冻结、占座、锁票等。这些业务的Try操作天然就是“检查预留”。不适合TCC的业务难以补偿或补偿成本极高的操作。例如发送短信、调用第三方支付一旦支付成功撤销成本很高。这类操作通常更适合“最大努力通知”或“本地消息表”方案。妥协方案对于“发送短信”这类操作如果非要纳入TCC可以这样设计Try阶段只记录要发送的消息和模板Confirm阶段实际调用短信网关发送Cancel阶段则什么都不做因为短信还没发。但这要求Confirm阶段的失败率极低且业务能接受极低概率下Confirm失败且无法重试短信未发送的情况。5.5 监控、排查与人工干预再完善的系统也会有意外。你必须为TCC事务建立监控和人工干预通道。监控关键指标全局事务成功率、失败率。各阶段Try/Confirm/Cancel的平均耗时、失败次数。悬挂事务、空回滚事务的数量这些是网络问题的信号。提供事务查询与控制台集成Seata-Server的控制台可以方便地查看全局事务和分支事务的状态进行手动重试或强制回滚。这是运维的救命稻草。建立对账与修复机制对于最终仍失败的事务如Confirm一直重试不成功需要有定期的对账作业对比TCC事务日志和业务实际数据发现不一致并触发修复脚本。例如发现一个订单状态是TRY但对应的库存冻结记录已是CONFIRMED则手动将订单状态改为CONFIRMED。TCC模式将分布式事务的复杂性从基础设施层转移到了业务层给了我们极大的灵活性也带来了相应的复杂度。它的核心价值在于通过业务设计用最终一致性实现了接近强一致性的业务效果同时保持了系统的高可用和高性能。理解其原理谨慎处理空回滚、防悬挂和幂等性再结合像Seata这样的成熟框架你就能在分布式系统中游刃有余地处理那些令人头疼的数据一致性问题了。在实际项目中我建议先从一两个核心交易链路开始试点积累经验后再逐步推广。