遇到分布式事务,这四种方案可以让你眉开眼笑~

大家好,我是飘渺。今天继续更新DDD&微服务专栏,本篇将跟大家探讨一下微服务中一项不可忽视的议题 - 分布式事务。

在微服务架构中,分布式事务是一项必然会面对的挑战。举个例子,在DailyMart中,库存服务和订单服务不在同一节点上。当用户下单时,必须首先调用订单服务创建订单,然后再调用库存服务进行库存扣减。这就引发了分布式事务的问题。

在分布式系统工程实践中,为了实现分布式事务,我们通常会考虑以下四种方案。

两阶段提交(2PC)

分布式事务的定义要求在多台机器中保持数据一致性。然而,一台机器在执行本地事务时无法知道其他机器中本地事务的执行结果。因此,我们需要引入一个额外的组件来统一调度所有分布式节点事务的执行,让当前节点知道其他节点的任务执行状态,然后通过通知和表决的方式决定是提交(Commit)还是回滚(Rollback)。

这个独立的组件就是协调者(Coordinator)。

两阶段提交把事务分成两个阶段,分别是 Commit-request 阶段Commit 阶段,两阶段提交的流程如下:

Commit-request

  1. 协调者询问各参与者事务是否执行成功,参与者发回事务执行结果。
  2. 如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回事务。

Commit阶段

  1. 协调者向所有参与者发起事务提交通知。
  2. 各参与者收到通知后提交事务,释放资源。

两阶段提交存在的问题

两阶段提交有以下几个明显的问题:

1. 同步阻塞

在执行过程中,所有参与节点都是事务独占状态,当参与者占有公共资源时,那么第三方节点访问公共资源会被阻塞。

2. 单点问题

一旦协调者发生故障,参与者会一直阻塞下去。

3. 数据不一致

在第二阶段中,假设协调者发出了事务 Commit 的通知,但是由于网络问题该通知仅被一部分参与者所收到并执行 Commit,其余的参与者没有收到通知,一直处于阻塞状态,那么,这段时间就产生了数据的不一致性。

虽然存在这些问题,但由于实现相对简单,很多解决方案仍然采用了两阶段提交的思想。例如,MySQL使用2PC来保证Binlog和InnoDB redo日志的一致性;RocketMQ的事务消息遵循两阶段提交的思想;Seata的AT模式也是基于两阶段提交的思想来实现。

事务补偿(TCC)

TCC(Try-Confirm-Cancel)是一种基于应用层的分布式解决方案,其核心思想是为每个操作注册相应的确认和撤销操作,将一个事务分为三个阶段:

Try阶段: 在Try阶段,主要进行业务系统的检测和资源预留。此阶段的目标是检查系统状态并进行必要的资源预留,为后续的确认和撤销阶段做好准备。

Confirm阶段: Confirm阶段用于确认业务的提交。一旦Try阶段执行成功并开始执行Confirm阶段,通常情况下,Confirm阶段不应该出错。在这个阶段,业务系统正式提交事务,完成之前预留的资源释放。

Cancel阶段: Cancel阶段在业务执行错误的情况下,用于执行业务取消并释放预留的资源。如果在Try阶段或Confirm阶段出现错误,系统将执行Cancel阶段,回滚事务,同时释放已经预留的资源。

在实现TCC时,通常需要借助专门的事务协调框架进行事务驱动。一些常见的开源框架包括ByteTCC、TCC-transaction和Himly。需要注意的是,TCC机制对业务入侵比较严重,每个操作都必须提供Try、Confirm、Cancel接口。

以DailyMart中订单创建业务为例,订单服务和库存服务都需要创建对应的三个阶段接口:

Try阶段: 在订单服务中,修改订单状态为创建中,这个状态在业务上没有实际意义,仅代表有人正在创建该订单。同时,在库存服务中,预扣库存的同时增加一个单独的列:库存冻结数量,表示库存准备预扣。

Confirm阶段: 在Confirm阶段,订单服务可以将订单状态修改为已创建,而库存服务则将冻结数量修改为0,表示正式完成库存的预扣。

Cancel阶段: Cancel阶段需要执行业务回滚逻辑。在订单服务中,可以直接删除订单;在库存服务中,需要回补库存数量。

可以结合下面的图直观感受一下:

本地消息表 + 补偿重试

在无需强一致性的分布式事务场景下,我们可以通过本地消息表+补偿重试的方式来确保最终一致性。该方案的执行原理如下:

  1. 在执行业务操作的同时,在本地消息表中插入一条状态为待发送 的记录,业务数据的记录与消息记录必须在同一个事务中完成,这是此方案的核心原则。由于消息表与业务表在同一个库中,事务可以通过数据库来保证。
  2. 事务提交后发送消息,如果消息发送成功,则将消息状态标记为发送成功或者直接删除消息。
  3. 在生产者服务中会创建一个定时任务,定时从消息表中检索待发送的消息重新发送。
  4. 对于消费者消费失败,则依赖MQ本身的重试机制来完成,保证数据的最终一致性。

其完整执行流程如下所示:

基于MQ的事务消息

另一种实现最终一致性的方式是通过支持分布式事务的消息队列,然而,目前主流消息队列中只有RocketMQ支持事务消息。

RocketMQ实现事务消息主要分为两个阶段:正常事务的发送及提交、事务信息的补偿流程

正常事务发送与提交阶段 :

1、生产者发送一个半消息给MQServer(半消息是指消费者暂时不能消费的消息) 2、服务端响应消息写入结果,半消息发送成功 3、开始执行本地事务 4、根据本地事务的执行状态执行Commit或者Rollback操作 事务信息的补偿流程 : 1、如果MQServer长时间没收到本地事务的执行状态会向生产者发起一个确认回查的操作请求 2、生产者收到确认回查请求后,检查本地事务的执行状态 3、根据检查后的结果执行Commit或者Rollback操作 。、

补偿阶段主要是用于解决生产者在发送Commit或者Rollback操作时发生超时或失败的情况。

可通过下图直观理解执行过程:

小结

本文介绍了分布式系统中常见的四种解决方案,用于处理复杂的分布式事务问题。实事求是地说,这些方案都存在一定的局限性,而且在实际实现过程中并不总是那么轻松。

有一位经验丰富的专家曾言:"解决分布式方案的最佳方法就是避免产生分布式事务。"建议在模块划分的初期,尽量避免涉及频繁交互的业务跨足多个模块。通过将相关业务划分到一个模块中,可以依赖数据库的事务特性来简化事务处理。然而,这种理想的解决方案通常只存在于理论中。一旦遇到无法避免的分布式事务问题,可以根据具体业务情况,选择本文介绍的几种分布式事务方案。

DailyMart是一个基于 DDD 和Spring Cloud Alibaba的微服务商城系统,采用SpringBoot3.x以及JDK17。旨在为开发者提供集成式的学习体验,并将其无缝地应用于实际项目中。该专栏包含领域驱动设计(DDD)、Spring Cloud Alibaba企业级开发实践、设计模式实际应用场景解析、分库分表战术及实用技巧等内容。如果你对这个系列感兴趣,可在本公众号回复关键词 DDD 获取完整文档以及相关源码。

相关推荐
sniper_fandc1 小时前
SpringBoot系列—入门
java·spring boot·后端
Piper蛋窝8 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
Bug退退退1239 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
prince0510 小时前
Kafka 生产者和消费者高级用法
分布式·kafka·linq
六毛的毛10 小时前
Springboot开发常见注解一览
java·spring boot·后端
AntBlack10 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
315356691310 小时前
一个简单的脚本,让pdf开启夜间模式
前端·后端
uzong11 小时前
curl案例讲解
后端
菜萝卜子11 小时前
【Project】基于kafka的高可用分布式日志监控与告警系统
分布式·kafka
一只叫煤球的猫11 小时前
真实事故复盘:Redis分布式锁居然失效了?公司十年老程序员踩的坑
java·redis·后端