微服务拆了单体,事务一致性怎么办?从理论到实践的深度解析

loong
2026-01-13 / 0 评论 / 22 阅读 / 正在检测是否收录...

还记得第一次把单体应用拆成微服务时的兴奋吗?感觉世界都清爽了。但很快,一个现实问题就砸了过来:原来数据库里一个事务就能搞定的事,现在数据散落在不同的服务里,这个订单创建了,那个库存却没扣减成功,怎么办?

说实话,分布式事务一致性,是微服务架构下最让人头疼的挑战之一。它不像性能问题,加个缓存就能立竿见影。它关乎数据的正确性,是系统的基石,一旦出问题,可能就是资金损失或客诉。

别急着选方案,先问自己三个问题

很多团队一上来就研究TCC、Saga这些高大上的方案,其实有点本末倒置。我的建议是,先停下来,回答这三个问题:

  1. 业务上,到底需要多强的一致性? 是要求像银行转账一样,必须100%强一致(ACID),还是可以接受短暂的不一致,最终对得上就行(BASE)?
  2. 这个操作失败的概率有多高? 如果失败是极少数情况,那补偿的成本可能比预防的成本低得多。
  3. 你的团队能驾驭多复杂的方案? 一个理论上完美但实现复杂、维护困难的方案,可能会把团队拖垮。

想清楚这些,我们再来看地图。

主流方案全景图:从“强一致”到“最终一致”

分布式事务的解决方案,本质上是在一致性、可用性和复杂性之间做权衡。我们可以把它们放在一个光谱上。

光谱的左端:追求强一致性

这类方案试图在分布式环境下模拟出本地事务的效果。

  • 两阶段提交(2PC):老牌方案,数据库层面支持(如XA协议)。它的优点是标准、概念简单。但缺点太明显了:同步阻塞。在第二阶段提交完成前,所有资源都被锁住,性能差,而且在协调者单点故障时,参与者可能一直处于不确定状态。对于高并发的互联网应用,我基本不推荐。
  • 三阶段提交(3PC):2PC的改良版,引入了超时机制和预提交阶段,缓解了阻塞问题,但实现更复杂,且依然无法完美解决网络分区问题。实际应用较少。

坦白讲,在微服务架构下,强一致性方案往往代价高昂。我们开始把目光转向右边。

光谱的中间与右端:拥抱最终一致性

这是目前微服务领域的主流思路,承认分布式环境下的固有困难,通过其他机制来保证数据的最终正确。

1. TCC(Try-Confirm-Cancel)

这可能是最著名的业务层解决方案了。它把事务过程拆成三个阶段:

  • Try:预留资源。比如冻结库存、扣减优惠券额度、生成一个“待确认”的订单。
  • Confirm:确认执行。真正扣减库存、使用优惠券、确认订单。
  • Cancel:取消回滚。释放冻结的库存、恢复优惠券额度、取消订单。

它的精髓在于“业务侵入性”。你需要为每个参与服务设计这三个接口。好处是控制粒度细,避免了长事务锁。坏处是开发量大,每个服务都要实现正向和反向操作,而且要考虑空回滚、幂等、防悬挂这些异常情况。

实践建议:TCC适合对一致性要求高、执行时间较短的业务,比如交易核心链路。可以借助成熟的框架(如Seata)来简化编码,但业务逻辑的设计依然需要你亲力亲为。

2. Saga模式

Saga的思路很直接:把一个长事务拆成一系列本地事务,每个本地事务都有对应的补偿操作。执行时按顺序执行正向操作,一旦某个步骤失败,就按相反顺序执行之前所有步骤的补偿操作。

它有两种协调方式:

  • 编排(Choreography):每个服务自己监听事件并决定下一步做什么。事件驱动,松耦合,但逻辑分散,不好调试。
  • 协奏(Orchestration):引入一个集中的“协调者”来指挥整个流程。逻辑集中,好管理,但多了单点依赖。

Saga和TCC的核心区别:Saga的补偿操作是“业务逆操作”(比如退款、加回库存),而TCC的Cancel是释放Try阶段预留的资源。Saga通常不预留资源,所以可能发生“脏写”(比如库存超卖),需要在业务层通过校验等手段防范。

实践建议:Saga特别适合流程长、步骤多的业务,比如机票酒店打包预订。我个人更倾向于使用Orchestration模式,虽然引入了协调者,但可视化和可调试性带来的收益更大。

3. 本地消息表+可靠事件

这是一个非常经典且实用的“土办法”,其核心思想是将分布式事务问题转化为本地事务和消息可靠投递问题

具体怎么做?

  1. 业务服务A在执行本地事务时,将需要发送给服务B的消息,与业务数据一起,插入到同一数据库的“本地消息表”中。这是一个原子操作。
  2. 有一个后台任务,不断轮询本地消息表,将待发送的消息投递给消息队列。
  3. 服务B消费消息,处理业务,处理成功后,可以回调通知A,或者A主动查询状态来更新消息状态。
  4. 消息队列需要保证可靠性,并且消费端要实现幂等。

这个方案的优点是相对简单,对现有业务侵入小,利用了成熟的MQ中间件。缺点是消息表会带来数据库压力,且整体延迟相对较高。

实践建议:这是很多中型项目的首选方案,技术门槛低,容易理解和落地。确保消息的幂等消费是关键中的关键。

4. 事务消息

这是本地消息表的“升级版”,由消息中间件(如RocketMQ)原生提供支持。生产者先发送一个“半消息”到MQ,MQ会持久化此消息但不会投递给消费者;等生产者本地事务执行成功并确认后,此消息才变为可消费状态;如果本地事务失败,则回滚这条消息。

它把消息的可靠存储从自己的数据库转移到了MQ,更解耦。但对MQ有要求,且需要处理好事务回查等机制。

我的选择策略:没有银弹,只有权衡

经过这么多项目,我形成了一个简单的决策树:

  • 要求强一致,且涉及外部系统(如银行):优先考虑对账+补偿,而不是试图做分布式事务。
  • 核心短流程,资金相关:考虑TCC,控制力强。
  • 长业务流程,非瞬时强一致:Saga(Orchestration模式)是好朋友。
  • 大多数最终一致场景,追求快速落地:本地消息表或事务消息,准没错。
  • 最简单的情况:先想想能否通过业务设计规避,比如把相关数据收敛到同一个服务里。

别忘了这些“隐藏关卡”

选好方案只是开始,在实践路上还有几个必须跨越的坑:

  • 幂等性:网络超时、重试机制会导致请求重复,每个服务的接口都必须保证多次执行效果相同。这是分布式系统的基石。
  • 可观测性:一个事务流程涉及多个服务,你必须能快速追踪一个事务ID在所有服务中的执行路径和状态。链路追踪、业务日志聚合至关重要。
  • 补偿的代价:补偿操作本身也可能失败。需要有重试、告警,甚至人工介入的兜底机制。

写在最后

分布式事务一致性是一个权衡的艺术。它没有标准答案,只有适合你当前业务阶段、团队能力和运维成本的“较优解”。

我的经验是,从简单的方案开始。先用可靠事件模式把业务跑起来,在监控中观察不一致发生的频率和影响。如果影响可接受,那就维持现状;如果不可接受,再针对性地升级到TCC或Saga。

技术是服务于业务的。有时候,一个清晰的夜间对账脚本,比一个复杂的实时分布式事务系统,更能解决问题。

你目前在项目中,用的是哪种方案?遇到了哪些意想不到的挑战?

0