微服务中躲不过的坑 - 分布式事务

大家好,我是飘渺。今天继续更新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的微服务商城系统。我们将在该系统中整合博主其他专栏文章的核心内容。如果你对这两大技术栈感兴趣,可以在公众号 JAVA日知录 回复关键词 DDD 以获取相关源码。

相关推荐
神奇小汤圆15 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生24 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling25 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅27 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079728 分钟前
Spring Flux方法总结
后端
define952732 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
惊讶的猫2 小时前
OpenFeign(声明式HTTP客户端)
网络·网络协议·http·微服务·openfeign
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
鹏北海2 小时前
micro-app 微前端项目部署指南
前端·nginx·微服务