在微服务架构盛行的今天,“数据一致性”成为绕不开的核心难题。单体应用中依赖本地事务就能轻松保证的ACID特性,在分布式场景下因服务拆分、网络不可靠、节点故障等问题变得异常复杂。分布式事务的本质,就是要在多个独立的服务节点间,保证数据操作的原子性——要么所有操作全部成功,要么全部回滚。
目前业界有多种分布式事务解决方案,其中Saga模式与TCC方案因适配场景广、无中间件依赖(或轻依赖),成为微服务架构中的主流选择。本文将从核心原理、实现细节、示例代码、适用场景等维度,对两种方案进行全面对比,并拓展分布式事务的关键设计考量,帮助开发者根据业务实际选型。
一、分布式事务基础铺垫
在深入Saga与TCC之前,先明确两个核心理论,帮我们理解分布式事务的设计思路:
CAP理论:分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可兼得。由于网络分区是客观存在的,分布式事务方案本质上是在“一致性”与“可用性”之间做权衡。
BASE理论:是对CAP理论的补充,核心思想是“放弃强一致性,追求最终一致性”。大部分业务场景中,最终一致性足以支撑业务运行(如电商订单支付后,物流状态延迟更新),这也是Saga与TCC方案的核心理论基础。
分布式事务的核心挑战:如何在多个服务间协调操作,应对网络超时、服务宕机等异常,确保最终数据一致。接下来,我们分别拆解Saga与TCC的解决方案。
二、Saga 模式:长事务的分段补偿方案
2.1 核心原理
Saga模式的灵感源于数据库的“长事务分段提交”,其核心思想是:将一个分布式事务拆分为多个“本地事务步骤”(每个步骤对应一个微服务的本地事务),每个步骤执行完成后直接提交本地事务;如果某个步骤执行失败,则通过“补偿事务”反向回滚之前所有已完成的步骤,最终保证数据的最终一致性。
Saga模式有两个关键角色:
事务步骤(Step):分布式事务的正向执行单元,每个Step对应一个微服务的本地事务(如“创建订单”“扣减库存”“扣减余额”)。
补偿步骤(Compensation):每个Step对应一个反向补偿操作(如“取消订单”“恢复库存”“退还余额”),用于在后续Step失败时回滚已完成的操作。
根据协调方式的不同,Saga模式分为两种实现:
编排式Saga(Choreography):无中心协调器,每个服务通过事件通知触发下一个服务的Step,失败时由最后一个失败的服务反向触发所有前驱服务的补偿操作。
协同式Saga(Orchestration):有中心协调器(Orchestrator),协调器负责按顺序调用所有服务的Step,失败时由协调器统一触发补偿操作(推荐方案,逻辑更清晰、易维护)。
2.2 实现示例:电商下单场景
以电商“下单-扣库存-扣余额”场景为例,分布式事务要求:三个操作全部成功则事务完成,任意一个操作失败则全部回滚。我们采用协同式Saga实现,技术栈:Spring Boot + 基于注解的本地事务 + 中心协调器。
2.2.1 场景拆解
Step1:订单服务(OrderService)创建订单(本地事务,状态为“待支付”)
Step2:库存服务(InventoryService)扣减商品库存(本地事务)
Step3:支付服务(PaymentService)扣减用户余额(本地事务)
补偿操作:若Step2失败,执行OrderService的“取消订单”;若Step3失败,执行InventoryService的“恢复库存” + OrderService的“取消订单”。
2.2.2 代码实现
首先定义核心服务接口(各服务独立部署,此处简化为本地接口模拟):
// 1. 订单服务接口publicinterfaceOrderService{// 正向Step:创建订单StringcreateOrder(LonguserId,LongproductId,Integerquantity,BigDecimalamount);// 补偿Step:取消订单booleancancelOrder(StringorderId);}// 2. 库存服务接口publicinterfaceInventoryService{// 正向Step:扣减库存booleandeductStock(LongproductId,Integerquantity);// 补偿Step:恢复库存booleanrestoreStock(LongproductId,Integerquantity);}// 3. 支付服务接口publicinterfacePaymentService{// 正向Step:扣减余额booleandeductBalance(LonguserId,BigDecimalamount);// 补偿Step:退还余额booleanrefundBalance(LonguserId,BigDecimalamount);}实现各服务的本地事务(核心:每个Step和补偿操作都是独立的本地事务,用@Transactional保证原子性):
// 订单服务实现@ServicepublicclassOrderServiceImplimplementsOrderService{@AutowiredprivateOrderMapperorderMapper;@Override@Transactional(rollbackFor=Exception.class)publicStringcreateOrder(LonguserId,LongproductId,Integerquantity,BigDecimalamount){// 生成订单IDStringorderId=UUID.randomUUID().toString();// 插入订单记录(状态:待支付)Orderorder=newOrder(orderId,userId,productId,quantity,amount,"PENDING");orderMapper.insert(order);System.out.println("订单创建成功:"+orderId);returnorderId;}@Override@Transactional(rollbackFor=Exception.class)publicbooleancancelOrder(StringorderId){// 更新订单状态为“已取消”introws=orderMapper.updateStatus(orderId,"CANCELLED");System.out.println("订单取消成功:"+orderId);returnrows>0;}}// 库存服务实现(类似,省略部分代码)@ServicepublicclassInventoryServiceImplimplementsInventoryService{@AutowiredprivateInventoryMapperinventoryMapper;@Override@Transactional(rollbackFor=Exception.class)publicbooleandeductStock(LongproductId,Integerquantity){// 检查库存是否充足Inventoryinventory=inventoryMapper.selectByProductId(productId);if(inventory.getStock()<quantity){thrownewRuntimeException("库存不足");}// 扣减库存inventoryMapper.deductStock(productId,quantity);System.out.println("库存扣减成功:商品"+productId+",数量"+quantity);returntrue;}@Override@Transactional(rollbackFor=Exception.class)publicbooleanrestoreStock(LongproductId,Integerquantity){inventoryMapper.restoreStock(productId,quantity);System.out.println("库存恢复成功:商品"+productId+",数量"+quantity);returntrue;}}// 支付服务实现(类似,省略部分代码)@ServicepublicclassPaymentServiceImplimplementsPaymentService{@AutowiredprivateUserMapperuserMapper;@Override@Transactional(rollbackFor=Exception.class)publicbooleandeductBalance(LonguserId,BigDecimalamount){// 检查余额是否充足Useruser=userMapper.selectById(userId);if(user.getBalance().compareTo(amount)<0){thrownewRuntimeException("余额不足");}// 扣减余额userMapper.deductBalance(userId,amount);System.out.println("余额扣减成功:用户"+userId+",金额"+amount);returntrue;}@Override@Transactional(rollbackFor=Exception.class)publicbooleanrefundBalance(LonguserId,BigDecimalamount){userMapper.refundBalance(userId,amount);System.out.println("余额退还成功:用户"+userId+",金额"+amount);returntrue;}}核心:Saga协调器(负责按顺序执行Step,捕获异常并触发补偿):
@ServicepublicclassSagaOrchestrator{@AutowiredprivateOrderServiceorderService;@AutowiredprivateInventoryServiceinventoryService;@AutowiredprivatePaymentServicepaymentService;/** * 执行下单Saga事务 */publicbooleanexecuteOrderSaga(LonguserId,LongproductId,Integerquantity,BigDecimalamount){// 存储中间状态,用于补偿时获取参数(实际场景可存入数据库,支持故障恢复)StringorderId=null;try{// Step1:创建订单orderId=orderService.createOrder(userId,productId,quantity,amount);// Step2:扣减库存inventoryService.deductStock(productId,quantity);// Step3:扣减余额paymentService.deductBalance(userId,amount);// 所有步骤成功,事务完成returntrue;}catch(Exceptione){System.out.println("Saga事务执行失败,触发补偿:"+e.getMessage());// 按反向顺序执行补偿if(orderId!=null){// 若Step3失败(扣余额失败),需补偿Step2(恢复库存)和Step1(取消订单)// 若Step2失败(扣库存失败),只需补偿Step1(取消订单)// 此处简化:无论哪个步骤失败,都按最大范围补偿(实际可根据异常类型精准判断)paymentService.refundBalance(userId,amount);// 若Step3未执行,补偿无影响(需保证幂等)inventoryService.restoreStock(productId,quantity);// 若Step2未执行,补偿无影响orderService.cancelOrder(orderId);}returnfalse;}}}2.2.3 关键说明
补偿顺序:必须按“反向顺序”执行补偿(Step3失败 → 补偿Step2 → 补偿Step1),确保数据回滚的正确性。
幂等性保障:补偿操作可能因网络重试被多次调用,需保证幂等(如取消订单时,先检查订单状态,避免重复取消)。
故障恢复:协调器若宕机,重启后需能恢复未完成的事务(可将事务状态存入数据库,重启后读取状态继续执行)。
三、TCC 方案:基于预留的细粒度补偿方案
3.1 核心原理
TCC是“Try-Confirm-Cancel”的缩写,其核心思想是:将分布式事务的操作拆分为三个阶段,通过“资源预留”的方式,在第一阶段锁定资源,第二阶段确认提交,第三阶段取消释放资源,从而实现数据一致性。
TCC的三个核心阶段:
Try阶段:尝试执行事务,核心是“预留资源”(不实际提交数据,仅锁定资源或标记资源状态),确保后续Confirm或Cancel能正常执行。例如:扣减库存前,先将库存标记为“锁定状态”;扣减余额前,先将余额标记为“冻结状态”。
Confirm阶段:确认执行事务,核心是“提交资源预留”(将Try阶段预留的资源正式提交,完成业务操作)。Confirm阶段是幂等的,且必须保证成功(通常是简单的状态更新,失败需重试)。
Cancel阶段:取消执行事务,核心是“释放资源预留”(将Try阶段预留的资源回滚释放,恢复原始状态)。Cancel阶段也是幂等的,失败需重试。
TCC与Saga的核心区别:TCC在Try阶段就完成了资源预留,补偿逻辑更细粒度、更高效;而Saga是在正向操作完成后,通过反向操作回滚,补偿逻辑是“反向业务操作”(如创建订单 → 取消订单)。
3.2 实现示例:同样电商下单场景
仍以“下单-扣库存-扣余额”为例,采用TCC方案实现,核心是为每个服务实现Try-Confirm-Cancel三个接口。
3.2.1 场景拆解
Try阶段:订单服务创建“待确认”订单;库存服务锁定商品库存;支付服务冻结用户余额。
Confirm阶段:订单服务将订单状态改为“已确认”;库存服务正式扣减锁定的库存;支付服务正式扣减冻结的余额。
Cancel阶段:订单服务将订单状态改为“已取消”;库存服务释放锁定的库存;支付服务解冻冻结的余额。
3.2.2 代码实现
首先定义TCC接口(每个服务需实现Try-Confirm-Cancel三个方法):
// 1. 订单服务TCC接口publicinterfaceOrderTccService{// Try:创建待确认订单@TwoPhaseBusinessAction(name="orderTcc",commitMethod="confirmOrder",rollbackMethod="cancelOrder")StringtryCreateOrder(@BusinessActionContextParameter(paramName="userId")LonguserId,@BusinessActionContextParameter(paramName="productId")LongproductId,@BusinessActionContextParameter(paramName="quantity")Integerquantity,@BusinessActionContextParameter(paramName="amount")BigDecimalamount);// Confirm:确认订单voidconfirmOrder(BusinessActionContextcontext);// Cancel:取消订单voidcancelOrder(BusinessActionContextcontext);}// 2. 库存服务TCC接口publicinterfaceInventoryTccService{@TwoPhaseBusinessAction(name="inventoryTcc",commitMethod="confirmDeduct",rollbackMethod="cancelDeduct")booleantryDeductStock(@BusinessActionContextParameter(paramName="productId")LongproductId,@BusinessActionContextParameter(paramName="quantity")Integerquantity);voidconfirmDeduct(BusinessActionContextcontext);voidcancelDeduct(BusinessActionContextcontext);}// 3. 支付服务TCC接口(类似,省略)publicinterfacePaymentTccService{@TwoPhaseBusinessAction(name="paymentTcc",commitMethod="confirmDeduct",rollbackMethod="cancelDeduct")booleantryDeductBalance(@BusinessActionContextParameter(paramName="userId")LonguserId,@BusinessActionContextParameter(paramName="amount")BigDecimalamount);voidconfirmDeduct(BusinessActionContextcontext);voidcancelDeduct(BusinessActionContextcontext);}说明:此处使用了Seata框架的TCC注解(@TwoPhaseBusinessAction),简化了协调逻辑。实际项目中也可自定义协调器,但Seata等框架已成熟,推荐直接使用。
实现TCC接口(核心:Try阶段预留资源,Confirm/Cancel阶段操作预留资源):
// 订单服务TCC实现@ServicepublicclassOrderTccServiceImplimplementsOrderTccService{@AutowiredprivateOrderMapperorderMapper;@OverridepublicStringtryCreateOrder(LonguserId,LongproductId,Integerquantity,BigDecimalamount){// Try阶段:创建“待确认”订单(预留订单资源)StringorderId=UUID.randomUUID().toString();Orderorder=newOrder(orderId,userId,productId,quantity,amount,"PENDING_CONFIRM");orderMapper.insert(order);System.out.println("订单预留成功:"+orderId);returnorderId;}@OverridepublicvoidconfirmOrder(BusinessActionContextcontext){// Confirm阶段:将订单状态改为“已确认”StringorderId=context.getActionResult().toString();orderMapper.updateStatus(orderId,"CONFIRMED");System.out.println("订单确认成功:"+orderId);}@OverridepublicvoidcancelOrder(BusinessActionContextcontext){// Cancel阶段:将订单状态改为“已取消”StringorderId=context.getActionResult().toString();orderMapper.updateStatus(orderId,"CANCELLED");System.out.println("订单取消成功:"+orderId);}}// 库存服务TCC实现@ServicepublicclassInventoryTccServiceImplimplementsInventoryTccService{@AutowiredprivateInventoryMapperinventoryMapper;@OverridepublicbooleantryDeductStock(LongproductId,Integerquantity){// Try阶段:锁定库存(预留资源)// 1. 检查库存是否充足Inventoryinventory=inventoryMapper.selectByProductId(productId);if(inventory.getStock()<quantity){thrownewRuntimeException("库存不足");}// 2. 锁定库存(增加锁定数量字段)inventoryMapper.lockStock(productId,quantity);System.out.println("库存锁定成功:商品"+productId+",数量"+quantity);returntrue;}@OverridepublicvoidconfirmDeduct(BusinessActionContextcontext){// Confirm阶段:正式扣减锁定的库存LongproductId=Long.valueOf(context.getActionContext("productId").toString());Integerquantity=Integer.valueOf(context.getActionContext("quantity").toString());inventoryMapper.deductLockedStock(productId,quantity);System.out.println("库存确认扣减成功:商品"+productId+",数量"+quantity);}@OverridepublicvoidcancelDeduct(BusinessActionContextcontext){// Cancel阶段:释放锁定的库存LongproductId=Long.valueOf(context.getActionContext("productId").toString());Integerquantity=Integer.valueOf(context.getActionContext("quantity").toString());inventoryMapper.releaseLockedStock(productId,quantity);System.out.println("库存锁定释放成功:商品"+productId+",数量"+quantity);}}// 支付服务TCC实现(类似,省略部分代码)@ServicepublicclassPaymentTccServiceImplimplementsPaymentTccService{@AutowiredprivateUserMapperuserMapper;@OverridepublicbooleantryDeductBalance(LonguserId,BigDecimalamount){// Try阶段:冻结余额(预留资源)Useruser=userMapper.selectById(userId);if(user.getBalance().compareTo(amount)<0){thrownewRuntimeException("余额不足");}userMapper.freezeBalance(userId,amount);System.out.println("余额冻结成功:用户"+userId+",金额"+amount);returntrue;}@OverridepublicvoidconfirmDeduct(BusinessActionContextcontext){// Confirm阶段:正式扣减冻结的余额LonguserId=Long.valueOf(context.getActionContext("userId").toString());BigDecimalamount=newBigDecimal(context.getActionContext("amount").toString());userMapper.deductFrozenBalance(userId,amount);System.out.println("余额确认扣减成功:用户"+userId+",金额"+amount);}@OverridepublicvoidcancelDeduct(BusinessActionContextcontext){// Cancel阶段:解冻冻结的余额LonguserId=Long.valueOf(context.getActionContext("userId").toString());BigDecimalamount=newBigDecimal(context.getActionContext("amount").toString());userMapper.unfreezeBalance(userId,amount);System.out.println("余额冻结释放成功:用户"+userId+",金额"+amount);}}事务发起端(调用TCC接口,由Seata协调Confirm/Cancel阶段):
@ServicepublicclassOrderService{@AutowiredprivateOrderTccServiceorderTccService;@AutowiredprivateInventoryTccServiceinventoryTccService;@AutowiredprivatePaymentTccServicepaymentTccService;/** * 发起TCC分布式事务 */@GlobalTransactional// Seata全局事务注解,自动协调TCC三个阶段publicbooleancreateOrder(LonguserId,LongproductId,Integerquantity,BigDecimalamount){// 调用各服务的Try方法(Seata会记录事务状态)orderTccService.tryCreateOrder(userId,productId,quantity,amount);inventoryTccService.tryDeductStock(productId,quantity);paymentTccService.tryDeductBalance(userId,amount);returntrue;}}3.2.3 关键说明
资源预留:Try阶段必须保证资源可预留(如库存表需增加“锁定数量”字段,用户表需增加“冻结余额”字段),这是TCC能高效补偿的核心。
协调器作用:Seata等框架作为协调器,负责记录各服务Try阶段的执行状态,所有Try成功则触发Confirm,任意一个Try失败则触发所有服务的Cancel。
幂等与重试:Confirm和Cancel方法必须幂等(如通过事务ID判断是否已执行),协调器会对失败的Confirm/Cancel进行重试,直到成功。
四、Saga 模式与 TCC 方案核心对比
为了更清晰地展示两种方案的差异,我们从多个维度进行对比:
| 对比维度 | Saga 模式 | TCC 方案 |
|---|---|---|
| 核心思想 | 正向步骤 + 反向补偿(无资源预留,直接提交本地事务) | Try预留资源 + Confirm提交 + Cancel释放(细粒度资源控制) |
| 补偿逻辑 | 反向业务操作(如创建订单 → 取消订单),逻辑复杂(需覆盖所有正向操作的反向场景) | 资源释放操作(如锁定库存 → 释放库存),逻辑相对简单(仅操作预留资源) |
| 实现复杂度 | 低(无需修改业务表结构,仅需实现补偿方法) | 高(需修改业务表增加预留字段,实现三个阶段的接口) |
| 一致性保障 | 最终一致性(中间状态可能存在数据不一致,如订单创建后库存未扣减) | 强一致性倾向(Try阶段锁定资源,中间状态无脏数据) |
| 性能 | 较低(补偿操作是完整的业务操作,耗时较长) | 较高(Confirm/Cancel是简单的状态更新,耗时短) |
| 适用场景 | 长事务、业务流程复杂(如电商下单后需多个后续步骤)、不介意中间状态的场景 | 短事务、对一致性要求高(如金融支付)、能修改业务表结构的场景 |
| 依赖框架 | 可选(简单场景可自定义协调器,复杂场景可用Seata、Hmily) | 推荐使用框架(如Seata、Hmily),简化协调逻辑 |
五、拓展:分布式事务的关键设计考量
无论选择Saga还是TCC,都需要解决以下几个共性问题,否则会导致数据不一致:
5.1 幂等性设计
分布式环境下,因网络超时、服务重试等问题,同一个操作可能被多次执行。必须保证:多次执行的结果与一次执行的结果一致。
常见解决方案:
基于唯一标识(如事务ID):执行操作前先检查该标识是否已执行,避免重复执行。
基于状态判断(如订单取消前,先检查是否为“待支付”状态)。
5.2 空回滚与悬挂问题(TCC特有)
空回滚:某个服务的Try方法未执行,但Cancel方法被调用(如网络超时导致Try未执行,协调器触发Cancel)。解决方案:记录事务状态,Cancel时先检查Try是否已执行。
悬挂:某个服务的Try方法因网络延迟未及时返回,协调器触发Cancel后,Try方法又执行成功(导致资源被预留但无法确认)。解决方案:Cancel时标记事务状态为“已取消”,Try方法执行前检查状态,若已取消则直接返回失败。
5.3 故障恢复机制
协调器或服务宕机后,重启后需能恢复未完成的事务。解决方案:将事务状态(如Saga的步骤执行情况、TCC的三阶段状态)持久化到数据库,重启后读取状态继续执行。
5.4 其他分布式事务方案补充
除了Saga和TCC,还有以下方案可根据场景选择:
本地消息表:基于消息队列的最终一致性方案,适合简单场景(如订单创建后发送消息通知库存扣减),实现简单但耦合度较高。
事务消息(RocketMQ):将消息发送与本地事务绑定,确保本地事务成功后消息才被投递,可靠性高,适合异步通信场景。
Sentinel模式:强一致性方案,通过锁定资源+两阶段提交实现,但性能较差,适合对一致性要求极高的金融场景。
六、总结
Saga模式与TCC方案都是基于BASE理论的最终一致性解决方案,核心差异在于“是否预留资源”:
Saga模式适合业务流程复杂、不介意中间状态、希望快速实现的场景,优势是实现简单、无需修改表结构,劣势是补偿逻辑复杂、性能一般。
TCC方案适合对一致性和性能要求高、能接受修改表结构的场景(如金融支付),优势是补偿逻辑简单、性能好,劣势是实现复杂、依赖框架。
实际选型时,需结合业务场景的一致性要求、性能需求、开发成本等因素综合判断。无论选择哪种方案,都必须重视幂等性、故障恢复等细节设计,才能真正保证分布式事务的可靠性。