【架构实战】分布式事务:从CAP定理到Seata实战,一文讲透跨服务数据一致性
【架构实战】分布式事务从CAP定理到Seata实战一文讲透跨服务数据一致性昨天聊了微服务架构下的 API 网关和认证授权有个核心问题一直没展开跨服务的数据一致性。一个典型的事故场景下单服务减了库存微服务 A 通知支付服务收款用户支付成功后发货服务要生成物流单。如果中间任何一步失败了怎么处理库存减了但钱没收、钱收了但货没发——这些半成品状态是企业级系统里最让人头大的问题。分布式事务说白了就是在多个服务、多个数据库之间保证要么全做要么全不做。这个问题困扰了架构师十几年到今天也没有银弹但有成熟可落地的解法。这篇从 CAP 定理开始到 TCC、Saga 模式再到生产级框架 Seata把完整链路讲清楚。一、CAP 定理理解分布式系统一致性的基础任何讨论分布式事务的文章不讲 CAP 定理就是在耍流氓。CAP 定理由加州大学伯克利分校的 Eric Brewer 在 2000 年提出由 Seth Gilbert 和 Nancy Lynch 在 2002 年证明。内容很简单在一个分布式系统里不可能同时满足一致性Consistency、可用性Availability和分区容错性Partition Tolerance三者最多同时满足两个。一致性Consistency每次读操作都能读到最新写入的数据所有节点看到的数据一致。可用性Availability每个请求都能在有限时间内得到响应不保证数据是最新的。分区容错性Partition Tolerance系统能在网络分区节点之间通信中断的情况下继续运行。为什么不能同时满足因为网络分区是一定会发生的机房断电、交换机故障、跨地域网络抖动一旦发生系统必须在停服等一致和继续服务之间二选一。没有第三个选项。CP 系统优先保证一致性牺牲可用性。典型ZooKeeper、HBase、Redis 主从同步模式从节点在同步未完成时拒绝读写。AP 系统优先保证可用性牺牲一致性。典型Cassandra、DynamoDB、Eureka注册中心、Redis 集群模式从节点可读但可能是旧数据。现实工程选择在网络分区必然发生的前提下工程上通常选择 AP然后通过补偿机制尽可能达到最终一致性。强一致性的代价太大了——跨机房同步锁在网络抖动时会让整个系统hang死这是用户无法接受的。二、强一致性方案两阶段提交2PC两阶段提交Two-Phase Commit, 2PC是最经典的强一致性协议理解它是理解所有其他方案的基础。2.1 流程阶段一Prepare准备阶段协调者Coordinator向所有参与者Participant发送 Prepare 请求询问你们准备好了吗。每个参与者在本地执行事务锁定资源、写 undo/redo 日志但不提交。然后返回 Ready 或 Abort。阶段二Commit提交阶段如果所有参与者都返回 Ready协调者发送 Commit所有参与者正式提交事务。如果任何一个返回 Abort协调者发送 Rollback所有参与者回滚。2.2 缺陷2PC 有三个致命问题生产环境基本不用同步阻塞Prepare 阶段参与者会锁定资源其他事务无法访问。如果某个参与者挂了协调者会一直等超时后才决定回滚。这期间整个系统处于锁定状态。单点故障协调者是唯一的如果它在 Commit 阶段挂了参与者会一直锁定资源等待指令。没有协调者谁都不敢提交也不敢回滚。数据不一致如果协调者发了部分 Commit有些发了有些还没发就挂了部分参与者提交了部分没提交数据就不一致了。工程结论2PC 不适合高并发互联网场景适合对一致性要求极高、参与节点少的金融/银行核心系统配合硬件和专网。普通业务用 2PC 就是给自己挖坑。三、柔性事务Seata 的 AT 模式和 TCC 模式3.1 什么是柔性事务柔性事务Flexible Transaction是对强一致性的妥协放弃实时强一致性追求最终一致性Eventually Consistency。即允许短暂的数据不一致但通过补偿机制在合理时间内自行修复到一致状态。SeataSimple Extensible Autonomous Transaction Architecture是阿里巴巴开源的分布式事务解决方案在国内生产环境使用非常广泛。它提供了四种模式AT 模式自动处理对业务无侵入基于 undo log 逆向 SQLTCC 模式Try-Confirm-Cancel三阶段编程式补偿Saga 模式长事务编排适合长链路、多步骤XA 模式强一致性基于 XA 协议需数据库支持国内工程实践的主流选择AT 模式常规业务 TCC 模式高并发金融场景。3.2 AT 模式自动化的数据补偿AT 模式是 Seata 最省心的模式业务代码几乎不用改。它的工作原理一阶段解析 SQLSeata 的 DataSource Proxy 拦截业务 SQL确定操作类型UPDATE/INSERT/DELETE和表名。根据主键生成逆向 SQLundo log写入undo_log表。二阶段提交提交成功后异步删除 undo log。由于全局锁已释放其他事务可以并发处理。异常回滚发生异常时Seata 从undo_log表读取逆向 SQL执行补偿恢复到修改前的状态。全局锁由 TCTransaction Coordinator管理。# seata-server.ymlTC 服务端配置server:port:8091store:mode:dbdb:datasourceType:druiddbType:mysqldriverClassName:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://mysql:3306/seata?useUnicodetrueusername:rootpassword:root!-- 各业务服务的依赖 --dependencygroupIdcom.seata/groupIdartifactIdseata-spring-boot-starter/artifactIdversion1.7.0/version/dependency# application.yml业务服务配置seata:tx-service-group:my_tx_groupenable-auto-data-source-proxy:trueconfig:type:nacosnacos:namespace:seataserverAddr:nacos:8848registry:type:nacosnacos:application:seata-serverserverAddr:nacos:8848业务代码完全不用改Seata 自动拦截 DataSource 操作ServicepublicclassOrderServiceImplimplementsOrderService{GlobalTransactional(namecreate-order,rollbackForException.class)OverridepublicvoidcreateOrder(CreateOrderRequestrequest){// 1. 创建订单记录OrderordernewOrder();order.setId(UUID.randomUUID().toString());order.setUserId(request.getUserId());order.setAmount(request.getAmount());order.setStatus(PENDING);orderMapper.insert(order);// 2. 调用库存服务扣减库存feign远程调用storageClient.deduct(request.getProductId(),request.getQuantity());// 3. 调用账户服务扣款accountClient.deduct(request.getUserId(),request.getAmount());// 4. 更新订单状态order.setStatus(PAID);orderMapper.updateById(order);// 任何一步抛异常Seata 自动回滚全部}}只需要加一个GlobalTransactional注解Seata 自动管理分布式事务。一阶段的 undo log 由框架自动生成、回滚时执行二阶段也是框架处理。3.3 TCC 模式精准控制的补偿AT 模式依赖 undo log适用于大多数场景。但如果需要精准控制每个阶段的业务逻辑或者对性能要求极高如每秒数万笔交易就需要 TCC 模式。TCC 把每个操作拆成三个阶段Try预留尝试执行锁定资源但不做最终确认。比如冻结库存、预扣金额。Confirm确认所有 Try 都成功后执行真正的业务操作。比如真正减库存、真正扣款。Cancel取消任何一个 Try 失败执行补偿释放资源。比如解冻库存、退还预扣金额。LocalTCCpublicinterfaceStorageTccService{TwoPhaseBusinessAction(namedeductStorage,commitMethodconfirm,rollbackMethodcancel)booleantryDeduct(BusinessActionContextParameter(paramNameproductId)StringproductId,BusinessActionContextParameter(paramNamequantity)intquantity,BusinessActionContextParameter(paramNamexid)Stringxid);booleanconfirm(BusinessActionContextcontext);booleancancel(BusinessActionContextcontext);}ServicepublicclassStorageTccServiceImplimplementsStorageTccService{AutowiredprivateStorageMapperstorageMapper;OverrideTransactionalpublicbooleantryDeduct(StringproductId,intquantity,Stringxid){// Try 阶段检查库存是否足够如果足够则预扣冻结StoragestoragestorageMapper.selectByProductId(productId);if(storage.getAvailable()quantity){thrownewBusinessException(库存不足);}// 冻结 quantity 库存available 减少frozen 增加storageMapper.freezeStock(productId,quantity);returntrue;}OverrideTransactionalpublicbooleanconfirm(BusinessActionContextcontext){// Confirm 阶段正式扣减冻结的库存StringproductIdcontext.getActionContext(productId,String.class);intquantitycontext.getActionContext(quantity,Integer.class);storageMapper.confirmFrozen(productId,quantity);returntrue;}OverrideTransactionalpublicbooleancancel(BusinessActionContextcontext){// Cancel 阶段释放冻结的库存StringproductIdcontext.getActionContext(productId,String.class);intquantitycontext.getActionContext(quantity,Integer.class);storageMapper.unfreezeStock(productId,quantity);returntrue;}}TCC 的关键设计原则幂等性Confirm 和 Cancel 都会重试必须设计成幂等的重复执行结果相同。用主键状态机判断或者用 Redis 记录 xid 做去重。空回滚Try 没执行成功网络超时Cancel 被调用了。此时不应该报错应该查一下有没有这个 xid 的记录没有就跳过。悬挂Cancel 比 Try 先执行比如网络问题 Try 超时了但 Cancel 消息先到了。此时 Confirm 不会执行Cancel 做了空回滚之后要防止 Try 最后又成功执行了。四、Saga 模式长事务编排当事务链路非常长10 步骤且每一步都有明确的正向和逆向操作时TCC 的复杂度会急剧上升。Saga 模式是更适合的选择。Saga 把长事务拆成一系列本地事务每个本地事务都有对应的补偿操作。执行顺序由编排器Choreography 或 Orchestration决定。编排模式Saga Orchestrator有一个中心编排器定义执行顺序。SagaStartpublicclassOrderSaga{AutowiredprivateSagaControllersagaController;publicvoidexecute(CreateOrderRequestrequest){sagaController.execute(newStep[]{newStep(createOrder,创建订单,this::createOrderStep,this::createOrderCompensate),newStep(deductStorage,扣减库存,this::deductStorageStep,this::deductStorageCompensate),newStep(deductAccount,扣减账户,this::deductAccountStep,this::deductAccountCompensate),newStep(sendMessage,发送通知,this::sendMessageStep,this::sendMessageCompensate),});}}事件驱动模式Saga Choreography每个服务通过消息队列MQ通知下一个服务不需要中心编排器。服务之间通过事件解耦适合松耦合的微服务架构但调试和追踪困难。五、生产环境落地避坑指南5.1 全局锁的性能问题Seata 的 AT 模式在 Update 操作时会加全局锁这是性能的主要瓶颈。高并发场景下如果一个全局锁持有时间过长会阻塞其他涉及同一行数据的事务。优化策略拆分热点数据不要把库存全放一张表按商品ID哈希分表让不同商品的扣减操作不竞争同一把锁。读写分离AT 模式默认读写都在主库可以考虑配置只读库分担读压力Seata 支持。降级方案库存扣减这种强一致性场景用 TCC查询类场景直接读从库不过 Seata。5.2 TCC 空回滚和悬挂的防御代码这是生产代码里最容易漏掉的部分一定要写Overridepublicbooleancancel(BusinessActionContextcontext){Stringxidcontext.getXid();StringproductId(String)context.getActionContext(productId);Integerquantity(Integer)context.getActionContext(quantity);// 空回滚防御查询是否存在 try 记录// 如果没有说明 try 根本没执行直接返回成功if(!frozenLogMapper.existsByXidAndProductId(xid,productId)){log.info(空回滚检测: xid{}, productId{}跳过,xid,productId);returntrue;}// 幂等防御检查是否已经 cancel 过if(frozenLogMapper.isCanceled(xid,productId)){returntrue;}// 执行解冻storageMapper.unfreezeStock(productId,quantity);frozenLogMapper.markCanceled(xid,productId);returntrue;}5.3 事务日志的可靠性Seata 的事务协调器TC Server状态存储推荐用数据库MySQL/PostgreSQL而不是文件或 Redis。数据库有 ACID 保证TC Server 重启后能恢复到正确状态。Redis 存储在服务器重启时可能丢数据导致事务状态不一致。5.4 监控告警分布式事务出了问题是很难排查的因为涉及多个服务。几个必须监控的指标全局事务成功率突然下降说明有异常分支事务超时率超时会导致全局回滚TC Server 的活跃会话数爆了说明并发过高补偿动作频率Cancel/Confirm 频繁执行说明业务失败率高六、选型建议什么时候用什么模式场景推荐模式原因大多数业务场景库存扣减、订单创建AT 模式对业务无侵入改造成本最低高并发金融支付强一致性要求TCC 模式精准控制性能好超长链路10步骤事件驱动架构Saga 模式避免长时间锁定强一致性要求数据库支持 XAXA 模式两阶段提交数据库原生支持一个系统的不同业务模块完全可以混用不同模式订单创建用 AT 模式通用支付清算用 TCC 模式金融级精准长账期对账用 Saga 模式链路长。七、总结分布式事务的核心矛盾强一致性 vs 高可用。没有银弹只有取舍。CAP 定理告诉我们分区一定会发生要在做 CP 还是 AP 之间做选择。2PC强一致但有致命缺陷同步阻塞、单点故障不适合互联网高并发场景。AT 模式最省心适合 80% 的业务场景Seata 自动处理回滚。TCC 模式精准可控适合金融支付类高一致性场景但需要写三阶段补偿代码。Saga 模式适合长链路编排MQ 驱动解耦但调试困难。工程实践里绝大多数场景用 AT 模式就够了。只有在真正遇到性能瓶颈或强一致性要求时才值得为 TCC 的复杂度买单。记住最合适的架构是能够解决你当前问题的最简单方案而不是技术最先进的那一个。我是做架构的关注我一起搞定分布式系统里的那些坑。