
做金融系统的人逃不开分布式事务这四个字。账户动账、开户绑卡、理财申购随便拎一个出来都是跨库跨服务的长链路数据一旦对不上账差比天塌下来还麻烦。最近几年从2PC到Saga模式再到基于Seata框架的落地实践我完整走过一遍演进和改造的过程也踩过不少教科书上根本不会写的坑。这篇文章不打算泛泛讲概念而是把当时做技术选型、状态机定义、灰度发布、上线后监控的完整过程整理成一份可以直接参考的落地笔记。如果你正在做金融级交易系统或者在微服务架构里纠结全局事务到底怎么处理这份笔记应该能帮你少走一大段弯路。1. 金融级核心链路里分布式事务到底卡在哪一环1.1 最该强一致的系统反而不能一刀切强一致金融系统对数据一致性的要求在所有行业里几乎是最高的。账少了不行账多了更不行每一分钱都要有迹可循。这是单体架构时代最舒服的地方——一个库、一张表本地事务一开提交失败就回滚数据天然是一致的。但微服务拆分之后平衡被打破了。账户域是一个库交易域是一个库清算、通知、风控各自有各自的存储一个业务请求从进来到结束可能横跨四五个服务的多张表。原本本地事务能解决的原子性现在没人能拍胸脯保证。我见过很多团队的第一反应是要不要上个分布式事务中间件但真正做过核心链路的人都知道越是在金融这种强调强一致的场景里越不能无条件追求全局强一致。因为强一致是要拿锁、拿阻塞、拿可用性去换的这个交易划不划算得先把业务摸透才能回答。1.2 一个典型动账场景拆开看事务边界拿一个最简单的跨系统转账来说A账户扣款、B账户入账。听起来就是两步实际落在系统里却是账户系统扣减A余额、账户系统增加B余额、交易系统生成流水、通知系统发送动账通知每一步还都可能涉及数据库、缓存、外部接口调用。如果扣款成功了但B账户入账失败怎么办如果交易流水没生成但余额已经动了怎么处理这些在单体架构里根本不是问题的问题在分布式环境下每一个都是事故隐患。所谓分布式事务要管的正是这条链路上多个参与者之间的原子性——要么一起成功要么通过某种机制把所有已成功的部分恢复到业务一致的状态。这里要先明确一个概念金融场景讲的一致性不等于数据库层面的强一致更多时候是账务一致性即业务上最终看上去是平的。这就给后续选择Saga模式埋下了最重要的伏笔。1.3 金融事务和普通互联网事务的差异决定了方案不是同一种我把两类场景放在一起对比过差别非常明显。维度普通互联网事务金融级事务一致性要求多数可以容忍短时最终一致账务侧要求严格一致过程可查可控并发总量高并发但单笔链路过短冲突压力集中在热点数据并发未必爆炸但单链路长参与者多失败代价下单失败用户刷新即可资金错账需要对账、冲正、补偿周期长链路特征横向扩展容易服务间耦合浅强关联、强状态、外部依赖多超时敏感审计要求不需要全链路留痕每个分支步骤都要求可回溯、可解释这种差异直接导致了一个结果互联网那种最终一致就行异步慢慢追的玩法在金融核心账务里行不通但传统那种全局刚性事务的做法在长链路里也扛不住。大家最终都在找一个中间态这也是2PC、TCC、Saga这些方案轮番登场的根本原因。2. 2PC的机制与局限金融场景只能小范围用不能大面积铺2.1 两阶段提交到底怎么工作的锁又是怎么挂上去的两阶段提交也就是2PC是分布式事务最经典的起点。它把一个全局事务拆成两个阶段准备阶段和提交阶段。协调者先问所有参与者你们能不能提交参与者各自执行事务、写日志、加锁但不真正提交等所有参与者都回答能提交协调者再发出真正的提交指令。整个过程听起来天衣无缝但问题恰恰出在准备阶段成功但未提交这段时间。为了让后续提交一定能成功参与者必须持有资源锁通常是数据库行锁或表锁。锁一旦挂上其他事务只能排队等。在单机数据库里这个窗口非常短但到了分布式环境协调者要跨网络收集N个参与者的投票延迟被大幅放大锁的持有时间也跟着膨胀。这也是XA协议在金融系统里被反复讨论的原因——XA是2PC在数据库层的标准实现。它本身没有错但它天生是给短事务、少节点、可快速决策的场景设计的一旦参与者变多、网络抖动变多、业务链路变长锁的代价就失控了。2.2 协调者单点与阻塞窗口一次长时间锁表的完整复盘两阶段提交还有一个被低估的大麻烦协调者本身是单点。协调者一旦在准备阶段之后宕机所有参与者都停在我已准备好等最终指令的状态谁也不敢自作主张提交或回滚。这个时候事务持有的锁根本释放不了。我经历过的场景是这么回事某个日终批处理链路里用了一个类似2PC的流程其中一个参与者数据库在准备阶段完成后网络出现了几分钟抖动协调者发出的提交指令没送达到。参与者这边等不到协调者指令只能一直等。正常情况下几分钟后连接恢复会自动继续但那次协调者服务的进程刚好同时发生了重启重连之后判定不了这个全局事务到底应该提交还是回滚因为没有可靠的持久化状态可以恢复。结果是一行账户余额记录被锁了将近四十分钟。这四十分钟里所有请求涉及这个账户的交易全部排队边上柜面人员的电话已经被打爆了。最终是人工介入查出事务ID手动补了一条提交指令才解开。这次经历让我彻底明白了一个道理2PC的逻辑正确性依赖于协调者永远可靠而现实里协调者跟业务服务一样同样会宕机、会重启、会遇到网络分区。在金融场景里这种不可控窗口是不可接受的。2.3 除了阻塞2PC被低估的脑裂和日志问题阻塞是最直观的问题但还有两个更深层的隐患很多文章一句话带过实际踩到才疼。第一个是脑裂。假设协调者还没来得及广播提交指令网络分区了。一部分参与者到了协调者所在的分区收到提交指令提交了另一部分参与者跟协调者断了连接超时之后默认全局事务失败自主执行回滚。两边做出的决策完全相反全局数据出现了不可调和的不一致。这在分布式系统里有一个专门的名字叫启发式异常一旦发生没有任何分布式协议能自动修复只能靠人工对账、补单、冲正。第二个是参与者宕机恢复后的状态判定问题。一个参与者在prepare阶段写入日志后宕机恢复之后它并不知道这个全局事务最终的结局是commit还是abort。如果协调者也正好没有持久化足够的信息双方就陷入死锁式的互相等待。所以真实生产环境中2PC必须搭配一套完备的全局事务日志、重放机制和人工运维工具这个成本在金融核心链路里往往比事务本身还要贵。这也解释了为什么后来大家宁可牺牲一部分强隔离性也要往Saga、TCC这些柔性方案上靠。不是2PC不好而是它的适用边界太窄只适合小范围、短周期、参与者很少且可控的场景。大部分金融业务链路天然就不在这个范围里。3. Saga为什么是长事务的破局点补偿不是回滚3.1 Saga的两个流派编排式和协同式我为什么更倾向编排Saga模式最早来自数据库领域的论文核心思想很简单把一个长事务拆成一系列有顺序的本地子事务每个子事务执行完立即提交如果后面某个子事务失败就通过执行对应的补偿事务把前面已经成功提交的子事务逐一反做回去。Saga在落地时分成两个流派。协同式是事件驱动的每个服务完成自己的步骤后发一个事件下一个服务监听事件继续执行谁失败谁发出反向事件由参与者自己往前追。服务之间没有中心控制点扩展性很好但全局流程很难一眼看懂出问题时排查链路异常痛苦。我在一个早期的模拟项目里试过协同式上线第一周就被一个补偿事件被消费了两次的问题搞得焦头烂额。从那以后我对协同式Saga的落地价值就打了一个大大的问号。编排式则完全不同。它引入了一个中心化的编排器由编排器记录当前走到哪个状态、下一步应该调用谁、失败之后应该进哪个补偿节点。所有分支的推进和回滚都听编排器统一调度流程像一张明确的状态机图每一步都可以被观测、被记录、被重放。Seata的Saga模式走的正是编排式这条路这也是它在金融场景里更受青睐的原因——金融系统要的从来不只是正确还要可控、可查、可干预。3.2 从2PC换到Saga我们到底交换了什么很多文章只说Saga提高了可用性、解决了阻塞但没说清楚代价是什么。事实上从2PC切到Saga本质是一次能力交换。维度2PCSaga资源锁全程持有阻塞严重无全局锁子事务提交后立即释放隔离性较强中间状态对外不可见没有隔离中间状态对业务可见可用性协调者单点阻塞窗口可用性低参与者和编排器均可独立恢复可用性高吞吐能力受限于锁持有时间接近各子事务的本地吞吐一致性语义强一致最终一致(通过补偿达到账务一致)实现复杂度中间件接管侵入低需要为每个子事务设计补偿逻辑这张表里最扎眼的是隔离性。2PC里事务中间状态是别人看不见的Saga里不存在这回事——你的子事务一笔一笔真实提交了中间任何一个瞬间对端系统看到的都是部分完成的数据。这在金融核心账务里是绝对不允许的。所以Saga能落地靠的从来不是它本身而是业务设计上要有对应手段去弥补隔离性缺失。金融系统里最常见的做法就是冻结转出之前先冻结校验通过再扣减校验失败直接解冻。冻结本身就是一个业务状态它允许数据停在看起来没动但实际被预订的中间态而且这个中间态对业务是完全可控的。换句话说Saga落地的关键不在技术框架而在业务状态机的设计水平。3.3 补偿的幂等设计没有幂等就没有补偿Saga的补偿事务在真实环境里一定会被重试可能因为网络超时可能因为编排器重启可能因为下游服务重复消费。所以我常说一句话没有幂等设计的补偿就是灾难放大器。空补偿是最典型的场景。补偿指令发出去了但正向事务因为超时压根没执行成功补偿到达时发现没有可补偿的数据。这个时候不能抛异常必须正常返回成功否则编排器会一直重试重试多了还可能把状态机推进到错误分支。再比如悬挂正向服务因为网络原因延后才执行成功但此时整个Saga已经走了补偿路径这笔正向执行就成了一次幽灵操作必须有一个机制能识别出这个事务已经不需要了。解决这些问题靠的是一套通用幂等规范。我这边是这么做的所有参与Saga的服务无论正向还是补偿都必须接收一个全局事务ID每个服务维护一张幂等表以全局事务ID服务名动作类型作为唯一键操作前先插幂等记录插入成功才允许执行业务操作所有补偿接口的返回都分三类——成功、失败、无需处理。这样无论请求重发多少次、网络怎么抖最终落库的业务动作只有一次。这套设计做完Saga的可靠性才真正达到了可上线的水平。它不复杂但缺了它Saga在金融场景里基本等于裸奔。4. Seata落地前的模式选型AT、TCC、Saga、XA到底该用哪种4.1 先搞清楚Seata的组件模型再谈模式选择Seata的架构可以从三个组件去理解TC是事务协调者负责全局事务的注册、驱动和状态流转TM是事务管理器负责在业务侧定义全局事务的边界决定最终提交还是回滚RM是资源管理器负责管理每个分支事务的资源向TC上报分支状态。围绕这三个组件Seata同时提供了四种事务模式XA、AT、TCC、Saga。这是一个非常容易让人迷惑的地方很多人以为Seata就是某个固定方案实际上它是把刚性事务和柔性事务都收纳进了同一个框架。XA模式走的是数据库层面的两阶段提交事务隔离性最强但同样有锁和阻塞问题。AT模式是Seata自己的自动补偿方案通过记录undo_log来实现一阶段提交、二阶段回滚侵入性小但依赖全局锁有脏读风险。TCC模式要求业务方自己实现Try、Confirm、Cancel三个方法灵活但编码量大。Saga模式则是我们前面重点讨论的编排式长事务方案。这四种模式各有各的适用面选错了后面全是坑。有些团队一上来就选AT因为代码改动最少但完全没想过AT模式中间会读到脏数据这在金融场景某些环节是致命的。4.2 按金融业务特征做模式选型我从实际场景中学到的判断逻辑根据我自己的落地经验金融业务里的分布式事务可以按以下逻辑去选型。业务特征推荐模式原因数据库异构、允许自动代理 SQLAT侵入最小适合内部运营类业务需要强隔离、事务周期短、节点少XA保留数据库级事务语义适合账务核心内的局部操作业务方明确具备 Try/Confirm/Cancel能力TCC控制力最强适合对隔离性有严格要求的关键链路跨系统长链路、参与者多、链路不可拆Saga无全局锁适合业务状态本身可逆的流程型链路以我待过的几个项目来看金融团队最谨慎的地方在于对脏读的容忍度。AT模式在二阶段回滚前全局锁未释放期间其他事务可能读到一阶段已经提交但最终会被回滚的数据。这在交易旺季的高并发下哪怕概率很低一旦发生就是账务级别的风险。因此实际生产里核心动账链路很少用AT更多会用TCC或者Saga配合对账兜底。TCC和Saga之间的选择也有讲究。TCC适合那种每个分支都能明确拆成预留、确认、取消的环节比如库存冻结。Saga适合正向动作已经自然完成、失败靠反向业务动作恢复的链路比如先冻结再扣减这个冻结-解冻本身就是业务动作不需要再造一套预留逻辑。我把两类业务列在一起对比之后通常结论都是Saga更契合金融长链路的形态。4.3 高可用部署和状态存储TC集群不落盘等于没做高可用模式选完紧接着是部署。Seata的TC是全局事务的中枢它必须高可用但高可用不是多起几个节点就可以。TC在运行过程中要记录全局事务状态、分支事务状态、锁信息这些状态如果只存在内存里节点一挂就全部丢失再多的副本也白搭。我见过最小可用的生产配置是这样做的TC多节点部署注册中心用独立集群事务状态存储放到独立的数据库。两个关键点——第一状态库绝不能跟业务库混用否则TC的一次慢查询就能拖垮核心业务第二状态库本身要做主备和备份因为它是分布式事务恢复的唯一依据。除了状态存储服务端还要配置合理的超时和重试参数。全局事务超时不能设得太大否则大量悬挂事务会积压在TC里成为隐患也不能太小否则长链路在高峰期稍微慢一点就被误判失败。高可用这件事上没有太多花活核心就是状态可恢复、节点可替换、依赖可容灾。把这三点做到位TC才能从能跑变成扛得住。5. 从定义状态机到灰度发布一个Saga链路的完整落地过程5.1 一个理财申购链路的状态机定义拆解纸上谈兵说了这么多实际操作一次最直观。我以一个简化的理财申购场景为例链路大致是风控检查开户状态冻结资金创建申购订单通知用户。这个链路的正向动作基本都落在账户和订单系统内每一段都可以设计出反向补偿非常适合用Saga编排。用Seata的Saga状态机来描述大概是下面这个结构。真实配置项还要结合版本微调但整体骨架就是这副样子。{ StateMachine: { Name: purchaseFlow, Comment: 理财申购正向与补偿编排, StartState: RiskCheck, States: [ { Name: RiskCheck, Type: ServiceTask, ServiceName: riskService, MethodName: riskCheck, Next: FreezeFund, CompensateType: none }, { Name: FreezeFund, Type: ServiceTask, ServiceName: fundService, MethodName: freeze, CompensateType: compensate, CompensateMethodName: unfreeze, Next: CreateOrder }, { Name: CreateOrder, Type: ServiceTask, ServiceName: orderService, MethodName: create, CompensateType: compensate, CompensateMethodName: cancel, Next: NotifyUser, IsEnd: false }, { Name: NotifyUser, Type: ServiceTask, ServiceName: notifyService, MethodName: sendMessage, CompensateType: none, IsEnd: true } ] } }这个定义里最需要注意的细节是不是每个正向节点都有补偿节点要看业务上是否真正可反做。风控检查本身不产生数据变更失败就失败不需要补偿通知用户这种动作失败也不影响账务一般只记录告警极端情况下最多补一个短信通知也不值得反向操作。如果一个节点不需要补偿就把CompensateType设为none让状态机知道走到这里失败时不需要触发任何反向调用。另一个重点是编排器对补偿顺序的控制。真实实现中补偿是严格按正向链路逆序触发的先执行用户通知前的补偿、再执行创建订单的补偿、最后执行解冻资金。这个顺序不能乱否则会出现订单取消了但资金还没解冻的中间状态给下游系统造成非常费解的错账幻觉。5.2 灰度发布与流量切流这个环节最怕一步到位Saga状态机开发完之后我从来不会让它一口气接管所有流量。这里的灰度不是可选项是必须项而且要多维度交叉验证。我的做法分四步走。第一步是全链路预发验证用压测工具模拟真实业务流量对每个正向节点分别注入失败看补偿链路能否正确触发、幂等表能否防住重复补偿。第二步是按业务维度灰度只放一小部分低价值、低风险的申购流量进新链路比如个人小额申购用零钱、钱包这类非核心资产做试点。第三步是按商户或用户粒度灰度逐步放大比例从5%到20%再冲到50%。第四步是准备好快速回滚开关一旦监控发现异常直接切回旧的事务处理逻辑这个过程要求在分钟内完成。灰度期间最容易忽略的是事务ID的透传。Saga编排器分发的每个参数都要带上全局事务ID但业务服务内部发起异步消息、调用网关接口时很多隐式调用链条并不会自动传递这个ID。如果下游某个环节拿不到全局事务ID幂等表就无法工作补偿就可能重复执行。所以我在灰度前置脚本里加了一条强制校验整条链路所有外部调用的日志里必须能关联到同一个全局事务ID查不到的一律视为链路断点不允许放量。5.3 监控指标和告警阈值安全感不是靠祈祷是靠数据Saga上线之后我觉得最值的投入就是监控。金融场景的事务链路长、参与方多没有监控出了问题就像在黑暗里捞针。我会把监控分成三个层次。第一层是全局事务维度核心指标包括全局事务启动数、提交数、回滚数、回滚率回滚率是最敏感的指标只要持续超过1.5%就必须立刻查原因。第二层是状态机维度包括每个状态的进入次数、当前停留数、状态机平均耗时、补偿触发次数如果某个状态长期有大量事务堆积说明下游服务出现瓶颈或阻塞。第三层是数据一致性的最终验证也就是对账——每天跑一次全链路对账核对正向操作与补偿操作影响过的每一行数据确保所有以回滚结束的事务都真正恢复到了业务一致状态。实际配置的告警阈值大概是这样一套全局事务回滚率连续三分钟超过2%触发P1告警补偿事务成功率低于95%触发P1状态机中某个状态停留事务数超过50且持续五分钟触发P2补偿调用平均耗时超过两秒触发P2。这套阈值在正式上线前要花一到两周时间在预发环境里反复调整因为不同链路的正常基线差异很大一刀切只会导致告警疲劳。5.4 实测中遇到的三个大坑补偿顺序、超时配置、中间态隔离每套系统上线都会遇到只属于它的坑Saga系统尤其如此。我这里踩过最典型的三个坑写出来给后来人提个醒。第一个坑是补偿顺序错乱。有一次我发现一个已完结的Saga实例在数据库里出现了订单已取消但资金处于半解冻状态排查下来是状态机配置里补偿节点的依赖关系写错了导致编排器在反向执行时并行调用了多个补偿服务。并行能加快速度但破坏了对中间顺序的保证。从那之后我的状态机定义里所有补偿动作一律串行按序执行性能慢一点没关系顺序正确永远是第一位的。第二个坑是超时配置一刀切。最开始我把所有分支的超时都设成了同样的五秒结果上游一个外部风控接口平时只要几百毫秒高峰期偶尔慢到八秒就被不断重试重试叠加直接把下游压垮。后来改成按服务的历史TP99动态设置超时同时重试次数收敛到最多三次并且重试之间必须退避。事务超时和分支超时是两套东西都要单独配置这个很多人容易混。第三个坑是中间态隔离不足。Saga没有全局锁意味着编排过程中业务对方能看到中间的已冻结未扣减状态。如果下游系统没有针对这类中间状态做兼容处理就会把它们误判为异常数据。解决方法是把所有中间状态在前端和接口层都显式定义出来——冻结中、解冻中、冲正中——让所有下游都知道这些是正常业务状态而不是数据错了。6. 写在最后别把Saga当银弹分布式事务是最后手段技术社区里经常看到终极方案这个说法我的看法是分布式事务从来不是终极方案它是所有其他手段都失效之后才轮得到的最后手段。我在金融系统里做过很长时间的架构工作最大的体会是一个优秀的架构不是善于解决分布式事务而是善于让分布式事务根本不发生。通过合理的服务划分、幂等设计、本地消息表、对账机制很多看似需要分布式事务的场景实际上可以通过架构层面的消解来规避。数据一致性问题是躲不掉的但分布式事务这个工具能少用一次就少用一次。如果业务确实无法规避必须跨服务、跨库、跨系统地完成一笔不可拆分的关键操作那Saga配合Seata确实是一个可靠的选择。它没有2PC那种锁阻塞和协调者单点问题只要做好业务状态机设计、补偿幂等、中间态冻结、监控告警和对账兜底完全可以在金融级的流量下长期稳定运行。我最后想分享的一点体会是分布式事务的难点从来不在中间件配置而在业务建模。把正向动作和反向动作的账务关系理清楚把每个补偿步骤设计得幂等且可观测这比纠结选什么框架重要得多。框架只是帮你把规则执行好规则本身永远要靠懂业务的人来定。