微服务拆解中复杂审批流的分布式事务实践:Saga模式与本地消息表
将老旧的单体应用拆解为微服务,尤其当核心业务逻辑涉及复杂且跨部门的审批流程,并且每个审批步骤都可能触及不同的数据库时,如何保证数据的最终一致性并实现平滑过渡,是架构师们面临的一大挑战。传统的两阶段提交(2PC)在微服务场景下通常不适用,因为它会导致高耦合、低可用性和性能瓶颈。我们需要寻找更适合微服务生态的分布式事务解决方案。
针对您描述的“跨部门复杂审批流程,每个步骤更新不同的数据库”的场景,以下几种分布式事务模式能够有效应对,并兼顾异构环境兼容性及运维复杂度:
1. Saga 模式:解决长事务的利器
Saga 模式是处理微服务中长事务(Long-running Transaction)的常用方法,它将一个分布式事务分解为一系列本地事务,每个本地事务负责更新一个服务的数据。如果任何一个本地事务失败,Saga 会通过执行补偿事务(Compensating Transaction)来撤销之前所有已完成的本地事务,从而恢复到初始状态。
工作原理:
Saga 模式有两种实现方式:
编排(Orchestration)Saga:
- 引入一个中央协调器(Orchestrator),负责管理 Saga 的整体流程。协调器会发送命令给各个服务执行本地事务,并监听它们的响应。根据响应结果,协调器决定是继续下一个步骤还是启动补偿事务。
- 优点: 流程逻辑集中,易于理解和管理;适用于复杂流程。
- 缺点: 协调器可能成为单点故障和性能瓶颈;引入额外的组件。
- 适用场景: 您的跨部门审批流程中,可以设计一个审批流程服务作为Orchestrator,协调用户服务、订单服务、库存服务等进行各自的本地事务更新。
协同(Choreography)Saga:
- 不设中央协调器,每个服务在完成自己的本地事务后,通过事件发布机制(如消息队列)通知下一个服务执行其本地事务。每个服务根据接收到的事件自行决定下一步操作,并在需要时触发补偿事件。
- 优点: 去中心化,服务间松耦合;高可用性和可伸缩性。
- 缺点: 流程逻辑分散在各个服务中,难以跟踪和理解整体流程;排查问题可能更复杂。
- 适用场景: 如果审批流程相对固定且服务自治性高,可以考虑这种模式。例如,审批通过后,库存服务发布“库存已扣减”事件,财务服务监听此事件进行记账。
兼容异构环境: Saga 模式的本质是基于本地事务和消息传递,因此对异构数据库环境非常友好。每个服务只需关注自己的本地事务和与消息队列的交互,无需关心其他服务使用何种数据库。
运维复杂度: 相较于2PC,Saga 模式的运维复杂度较高,需要处理消息可靠性、幂等性、补偿事务的实现等问题。但如果选择成熟的消息队列(如Kafka, RabbitMQ)作为基础设施,可以大大简化这部分工作。
2. TCC(Try-Confirm-Cancel)模式:服务层面的一致性保障
TCC 模式是一种经典的分布式事务解决方案,它在业务层面将一个完整的业务逻辑分为 Try、Confirm、Cancel 三个操作。
- Try 阶段: 尝试执行业务,完成所有业务资源的检查和预留(锁定资源)。
- Confirm 阶段: 所有 Try 都成功后,执行真正的业务操作,提交事务。
- Cancel 阶段: 如果任一 Try 失败,则执行补偿操作,释放 Try 阶段预留的资源。
工作原理:
TCC 模式要求每个参与的服务都实现 Try、Confirm、Cancel 三个接口。事务发起方会协调这些服务的调用。
优点: 强一致性,比 Saga 模式在某些情况下提供更强的隔离性;可以解决资源预留问题。
缺点: 业务代码侵入性强,实现复杂,需要为每个业务操作设计 Try/Confirm/Cancel 接口;对开发者要求高。
兼容异构环境: 同样基于业务逻辑实现,与底层数据库类型无关,因此兼容性良好。
运维复杂度: 实现和调试复杂,需要关注空回滚、悬挂、幂等性等问题。
适用场景: 对于您的审批流程中,如果某些核心步骤对资源预留和强一致性有较高要求,例如资金冻结或库存预留,可以考虑在这些特定子事务中采用TCC。但整体审批流程可能更适合Saga。
3. 本地消息表 / 发件箱模式(Outbox Pattern):确保事件最终一致性
此模式并非一个独立的分布式事务解决方案,而是确保“本地事务”与“消息发送”之间的原子性,是实现 Saga 模式(尤其是 Choreography Saga)和事件驱动架构的基石。
工作原理:
当服务需要发送一个业务事件时,它不会直接发送到消息队列,而是将该事件记录在自己的本地数据库的一个“发件箱表”(Outbox Table)中,这个操作与业务数据的更新在同一个本地事务中提交。然后,一个独立的服务(或一个定时任务)会扫描发件箱表,将未发送的事件发布到消息队列,并标记为已发送。
优点: 简单有效,避免了分布式事务管理器,通过本地事务保证了数据更新和事件发送的原子性,从而保证最终一致性。
缺点: 引入额外的数据库表和轮询机制,存在一定的延迟。
兼容异构环境: 只要服务能访问自己的本地数据库,即可实现,与数据库类型无关。
运维复杂度: 相对较低,主要在于发件箱表的维护和事件发送服务的可靠性。
适用场景: 这是您实现“每个步骤更新不同数据库,保证最终数据一致性”的关键。每个服务的本地事务提交后,将流程状态变更或审批事件写入本地消息表,再由后台任务将其发布出去,供其他服务消费。
总结与建议
考虑到您的核心诉求是“保证最终数据一致性”、“平滑过渡”、“兼容异构环境”并“避免引入过多运维复杂性”,我强烈建议您以 Saga 模式(优先考虑编排式)为主体,结合本地消息表/发件箱模式来实现复杂的跨部门审批流程。
- 顶层设计: 将整个审批流程定义为一个 Saga。
- 核心协调: 创建一个专门的“审批流程服务”作为 Saga 协调器(Orchestrator)。它负责维护审批流程的状态,向各个部门对应的微服务(如“用户服务”、“财务服务”、“业务部门服务”等)发送指令,并监听它们的响应。
- 服务实现: 每个参与审批的服务实现自己的本地事务。当本地事务成功时,将结果(如“审批通过”、“数据已更新”)写入本地消息表,并通过发件箱模式发布事件。如果需要回滚,服务也需实现相应的补偿逻辑。
- 消息通信: 使用可靠的消息队列(如 Kafka),确保事件的顺序性、可靠投递和可回溯性。
- 异构兼容: 由于各服务只通过消息队列和协调器交互,并且本地事务只在自己的数据库上进行,因此完美兼容异构数据库环境。
- 平滑过渡: 在微服务拆解初期,可以先将审批流程中几个最核心、最独立的步骤微服务化并采用 Saga 模式,其他部分仍由单体应用处理。随着拆解的深入,逐步将更多功能迁移到微服务中,并纳入 Saga 协调。
实践提示:
- 幂等性: 所有接收方服务必须实现消息的幂等性处理,避免重复消费消息导致数据不一致。
- 可观测性: 建立完善的日志、监控和链路追踪系统(如 ELK Stack, Prometheus, Jaeger),以便在分布式事务出问题时快速定位和解决。
- 人工干预: 对于特别复杂的异常情况,需要设计人工干预机制,例如失败事务的手动重试或回滚。
通过这种组合方案,您可以在微服务拆解过程中,以一种相对可控且弹性强的方式,解决复杂的分布式事务问题,同时为未来的系统扩展打下坚实基础。