前言
在当今的分布式系统中,分布式事务管理是一个关键挑战。在面对跨多个服务的复杂业务流程时,确保数据一致性和事务的原子性变得至关重要。本文将深入探讨分布式事务的概念、原理、实现方式以及在Java领域的应用。
什么是分布式事务
分布式事务是指涉及多个独立的系统或服务的事务操作。在分布式系统中,由于数据存储在不同的数据库或服务中,并且跨越多个计算节点,因此需要跨系统进行事务协调和管理,以确保事务的原子性、一致性、隔离性和持久性(ACID原则)。
可能语言文字描述的有点抽象,我用几张图片,教你弄懂分布式事务。
传统单体架构
在互联网发展初期,单体架构完全可以满足现有业务需求,所有的业务共用一个数据库,整个业务流程或许只用在一个方法里同一个事务下操作数据库即可。此时做到所有操作要么全部提交 或 要么全部回滚很容易。
@transactional
public void demoService(UserInfo userInfo) {
// 1.用户付款
userMapper.payMoney(userInfo);
// 2.订单增加
orderMapper.addItem(userInfo);
}
可随着业务量的不断增长,单体架构渐渐扛不住巨大的流量,时间久了,各种各样的问题自然而然的也出现了:复杂性高,部署频率低,可靠性差,扩展能力受限,此时就需要对数据库、表做分库分表处理,将应用服务进行拆分。也就产生了用户服务、订单服务、库存服务等,由此带来的问题就是服务与服务之间的独立部署,互相隔离,每个微服务都维护着自己的数据库,服务之间的调用只能通过RPC远程掉调用,此时单体架构的数据库事务就无法做到全局事务的管理。当用户再次进行付款服务的时候,此时不同的服务只能保证各自的数据库事务。所以为了保证整个支付流程的数据一致性,就需要分布式事务了。
分布式部署架构
了解分布式事务,就不得不提一下分布式事务中比较经典的一些理论知识。
CAP理论
CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)
在理论计算机科学中,CAP 定理指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:一致性(Consistency) : 所有节点访问同一份最新的数据副本可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。CAP 目前来说无法都兼备,因此当前微服务策略中要么要么CP,或者AP。
BASE理论
这个时候又有一个理论出现了,那就是 BASE理论 。它是用来对 CAP理论 进行一些补充,
BA(Basically Available):基本可用
S(Soft State):软状态
E(Eventually Consistent):最终一致性
这个理论的核心思想便是:如果我们如法做到强一致性,那么每个应用都应该根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
分布式事务解决方案
所以在分布式背景下,两个服务之间相互调用,使用的是不用的数据库,这种情况下肯定会出现分布式事务的问题。
2PC(两阶段提交)
基于 XA 协议实现的分布式事务,XA 协议中分为两部分:事务管理器和本地资源管理器。
两阶段提交是把整个事务提交分为 prepare 和 commit 两个阶段以支付系统为例,分布式系统中有用户、订单和库存三个服务,如下图:
第一阶段:prepare 阶段
第二阶段:commit 阶段
两阶段提交(2PC),对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。
2PC的缺点也是显而易见,它是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务。所以它比较适⽤于执⾏时间确定的短事务,整体性能比较差。
一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。
3PC(三阶段提交)
三段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的阻塞问题,上边提到两段提交,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞锁定资源。
为了解决两阶段提交的问题,三阶段提交做了改进:
- 在协调节点和事务参与者都引入了超时机制。
- 第一阶段的 prepare 阶段分成了两步,canCommit 和 preCommit。
第一阶段:canCommit
第二阶段:preCommit
第三阶段:commit 阶段
虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,如果第三阶段发出 rollback 请求,有的节点没有收到,那没有收到的节点会在超时之后进行提交,造成数据不一致。
TCC (Try Confirm Cancel) 事务补偿
TCC它是属于补偿型分布式事务。它的核心思想是 针对每个操作,都要注册一个与其对应的确认和补偿操作。TCC 实现分布式事务一共有三个步骤:分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
TCC是基于业务层面做的分布式事务,最终目的是达到数据最终一致性,是一种柔性事务。
下面以一个例子来说明三个阶段需要做的事:比如现在有两个数据库,一个用户账户数据库、一个商品库存数据库,现在提供一个买货的接口,当买卖成功时,扣除用户账户和商品库存,大致伪代码如下:
public void buy() {
// 用户账户操作
userAccount();
// 商品账户操作
StoreAccount();
}
在上面这个操作做,两个函数的操作必须同时成功,不然就会出现数据不一致问题,也就是需要保证事务原子性。因为设定的场景是数据在两个不同的数据库,所有没有办法利用单个数据库的事务机制,它是跨数据库的,所以需要分布式事务的机制。
class Demo {
public void buy() {
// try 阶段:比如去判断用户和商品的余额和存款是否充足,进行预扣款和预减库存
if (!userServer.tryDeductAccount()) {
// 用户预扣款失败,相关数据没有改变,返回错误即可
}
if (!storeService.tryDeductAccount()) {
// cancel 阶段: 商品预减库存失败,因为前面进行了用户预扣款,所以需要进入cancel阶段,恢复用户账户
userService.cancelDeductAccount();
}
// Confirm 阶段:try 成功就进行confirm阶段,这部分操作比如是将扣款成功状态和减库存状态设置为完成
if (!userService.confirmDeductAccount() || !storeService.confirmDeductAccount()) {
// cancel 阶段:confirm的任意阶段失败了,需要进行数据恢复(回滚)
userService.cancelDeductAccount();
storeService.cancelDeductAccount();
}
}
}
可以看出,如果后续增加更多的业务处理,就会重复添加三个阶段的业务代码,代码侵入量高。
业务状态补偿
业务状态补偿是通过给业务数据添加状态字段,在数据库中认为当前服务执行成功,标识成功,通过调用其它服务返回的业务结果,来对其本地业务进行补偿,失败则进行一些补偿操作,更新状态,从从而达到最终的数据一致性,这种方案大多在支付、银行业务场景见的比较多,很依赖业务实现方案。
MQ消息事务
消息事务的原理是 将两个事务通过消息中间件来进行异步解耦。基于可靠消息服务的方案是通过消息中间件来保证上、下游应用数据操作的一致性。假设有 订单服务、库存服务,分布可以处理 订单、库存两个任务,此时需要存在一个业务流程,将任务 订单和库存 放到同一个事物中处理,这种方式就可以借助消息中间件来实现。
上面大致分为两个步骤:
- 步骤一: 订单服务向消息中间件发布消息
1.在订单服务开始前,首先向消息中间件发送一条信息,告诉MQ自己即将开始执行相应的业务操作
2.消息中间件收到后将该消息持久化,但不进行投递。持久化成功后,向订单服务返回确认应答
3.订单服务收到确认应答后,便可以开始处理对应的业务
4.订单服务处理完后,便会向消息中间件发送 Commit 或者 Rollback 请求,该请求发送完成后,订单服务业务就算处理完成,该事务的处理过程也就结束了
5.在消息中间件收到 Commit 后,便会向 库存服务投递消息,如果收到 Rollback 便会直接丢弃消息
如果消息中间件在最后的过程中,长时间没有收到服务A 发送的 Commit 或 Rollback 指令,这个时候就需要依靠 超时询问机制
超时询问机制:
订单服务除了实现正常的业务流程之外,还是需要提供一个可供消息中间件事务询问的接口。在消息中间件第一次收到消息后便会开始计时,如果超过规定的时间没有收到后续的指令,就会主动调用订单服务提供的事务询问接口,询问当前服务的状态,通常来说该接口会返回三种结果,中间件需要根据这三种不同的结果做出不同的处理:
提交:直接将该消息投递给服务B
回滚:直接将该消息丢弃
处理中:继续等待,重新计时
- 步骤二: 消息中间件向库存服务投递消息
1.消息中间件收到订单服务的提交 Commit 指令后便会将该消息投递给库存服务,然后将自己的状态置为阻塞等待状态。库存服务收到消息中间件发送的消息后便开始处理自己的业务逻辑,处理完成后便会向消息中间件发出回应。但是在消息中间件阻塞等待的时候同样会出现问题
正常情况:消息中间件投递完消息后,进入阻塞等待状态,在收到确认应答后便认为事务处理完成,该流程结束
等待超时情况:在等待确认应答超时之后就会重新进行投递,直到库存服务器返回消费成功响应为止。而消息重试的次数和时间间隔都可以设置,如果最终还是不能成功进行投递,则需要人工干预。
由此可以看出来,MQ消息事务方案是实现了最终一致性,适用于高并发的场景。RocketMQ 就很好的支持了消息事务。如果只是为了实现MQ事务而引入MQ事务,势必会增加业务的复杂性,如果业务本身就因为其它需求,使用到了RocketMQ,消息事务方法不失为一种好的解决办法。
Seata (阿里开源分布式解决方案)
Seata是一个分布式事务解决方案,致力于解决分布式系统中的事务一致性问题。它提供了高效、易用的分布式事务管理功能,包括事务发起、全局事务管理、分支事务管理等。Seata具有原子性、一致性、隔离性和持久性(ACID)的特性
Seata 的设计目标是对业务无侵入,因此它是从业务无侵入的两阶段提交(全局事务)着手,在传统的两阶段上进行改进,他把一个分布式事务理解成一个包含了若干分支事务的全局事务。而全局事务的职责是协调它管理的分支事务达成一致性,要么一起成功提交,要么一起失败回滚。
Seata 架构设计
- TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
- TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
- RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
1.订单服务中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID
2.订单服务中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务
3.订单服务开始执行分支事务
4.订单服务开始远程调用库存服务,此时 XID 会根据调用链进行传播
5.订单服务中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
6.订单服务开始执行分支事务
7.全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚
8.TC 协调其管辖之下的所有分支事务,决定是提交还是回滚
SpringBoot集成Seata
// 在引入seata之前,需要下载相关服务,在官网下载即可。
引入 seata 的 maven 依赖坐标,需要开启事务的方法上添加 @GlobalTransactional 注解,类似于我们单体事务添加的@Transactional。
@GlobalTransactional
// 支付系统伪代码
public String demoService(UserInfoDTO dto) {
// 订单服务
orderService.addOrder(dto);
// 库存服务
// 模拟远程调用库存服务
}
Seata 作为一个强大的分布式事务解决方案,为我们提供了一种简单而灵活的工具来解决分布式事务问题。