SpringCloud Alibaba 2021微服务实战二十五 分布式事务

一、分布式事务

1、事务的概念

  事务是一组操作的执行单元,相对于数据库操作来讲,事务管理的是一组SQL指令,比如增加,修改,删除等,事务的一致性,要求,这个事务内的操作必须全部执行成功,如果在此过程种出现了差错,比如有一条SQL语句没有执行成功,那么这一组操作都将全部回滚

  最经典的例子便是:A向B汇款500元,B账户多了500元,这整个过程,要么全部正常执行,要么全部回滚,不然就会出现A扣款,B收不到钱,或者A没扣款,B收到500元的情况,这种场景是灾难性的。

严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。

  • 原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么都不执行。
  • 一致性(Consistency),可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据。
  • 隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。
  • 持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。

而通俗意义上事务就是为了使得一些更新操作要么都成功,要么都失败。

2,分布式事务的产生

分布式系统

分布式事务中的“事务”,和传统的数据库事务中的“事务”严格意义上已经不是完全等同的了。

分布式事务产生的场景

在分布式系统,数据库都会垂直拆分数据库,分为支付数据库、订单数据库、积分数据库、优惠全数据库等,同时服务微服务化,也会划分为商品微服务,订单微服务等等业务组成,会分为多个数据源,会产生分布式事物问题。

从上面来看,我们可以看为两块,一个是 service 产生多个节点,另一个是 resource 产生多个节点。

service 多个节点

随着互联网快速发展,微服务,SOA 等服务架构模式正在被大规模地使用。

举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额、积分、优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护。

这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。

resource 多个节点

同样的,互联网发展得太快了,一般来说,我们的 MySQL 装千万级的数据就得进行分库分表。对于一个支付宝的转账业务来说,你给别的朋友转钱,有可能你的数据库是在北京,而你朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。

spring事务和分布式事务的区别是什么? spring事务,本地事务 分布式事务是跨服务间的通讯(不同的数据库连接)都会产生分布式事物问题。

具体应用场景,包括以下三个:A、服务内跨数据库的事务;B、跨内部服务的事务;C、跨外部服务的事务。

其中划分内部和外部的标准是:内部服务我们可以控制其实现,修改配置或代码;外部服务指的是第三方的,只能约定通信的方式和具体协议,具体代码实现在控制范围之外。

具体如下:

应用场景A:服务内跨数据库

如下图所示,在同一个服务方法内,访问两个或两个以上数据库。

springcloud分布式事务框架 springcloud分布式事务实现_分布式事务

 

我们知道,Java事务是通过Connection对象控制的。不同的数据库,是不同的数据库链接,通过不同的Connection对象实现。传统数据库事务无法实现事务控制,需要引入事务协调者的概念。这是场景A,这个场景中分布式体现在数据库的部署上。

 

应用场景B:跨内部服务 如下图所示,

springcloud分布式事务框架 springcloud分布式事务实现_分布式事务_02

 

一个服务通过微服务框架或者RPC调用调用其他的服务,多个子服务需要同时成功或失败。每个子服务都有自己的持久化方式,不一定是数据库,体现事务的持久性。每个子服务部署在不同的服务容器中,不同的服务容器部署在不同的服务器节点上。这是场景B,这个场景中分布式体现在服务(或应用)的部署上。

这时候,事务的概念已经超出“数据库”的范畴了。

 

应用场景C:跨外部服务

springcloud分布式事务框架 springcloud分布式事务实现_数据库_03

 

这个场景是在应用场景B的基础上,进一步,服务的具体实现在我们控制范围之外。我们不能限制其实现语言,不能要求指定方法上加标注(注解)。甚至除了服务调用的网络通道外,我们不能期望服务间访问相同的Zookeeper作为事务协调器。这是场景C,这个场景中,我们只能在通信协议层面做约定,是最彻底的分布式场景。

 

3,解决分布式事务理论基础

分布式事务中的“事务”要区分与数据库事务,实现分布式事务的理论主要为CAP理论及BASE理论。

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

  • 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
  • 可用性(Availability) : 每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

这个定理在迄今为止的分布式系统中都是适用的

我们来分析一下数据库的两阶段提交。

对数据库分布式事务有了解的同学一定知道数据库支持的2PC,又叫做 XA Transactions。

MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。

其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:

  • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
  • 第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。这样做的缺陷是什么呢? 咋看之下我们可以在数据库分区之间获得一致性。

如果CAP 定理是对的,那么它一定会影响到可用性。

如果说系统的可用性代表的是执行某项操作相关所有组件的可用性的和。那么在两阶段提交的过程中,可用性就代表了涉及到的每一个数据库中可用性的和。我们假设两阶段提交的过程中每一个数据库都具有99.9%的可用性,那么如果两阶段提交涉及到两个数据库,这个结果就是99.8%。根据系统可用性计算公式,假设每个月43200分钟,99.9%的可用性就是43157分钟, 99.8%的可用性就是43114分钟,相当于每个月的宕机时间增加了43分钟。

以上,可以验证出来,CAP定理从理论上来讲是正确的。

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要性比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)

4,分布式事务解决方案:

在说方案之前,首先一定要明确你是否真的需要分布式事务?

上面说过出现分布式事务的两个原因,一个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪,而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。

因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。

如果你确定需要引入分布式事务,可以看看下面几种常见的方案。

两阶段提交(2PC)

熟悉mysql的同学对两阶段提交应该颇为熟悉,mysql的事务就是通过「日志系统」来完成两阶段提交的。

两阶段协议可以用于单机集中式系统,由事务管理器协调多个资源管理器;也可以用于分布式系统,「由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交」。

springcloud分布式事务框架 springcloud分布式事务实现_分布式事务_04

这个协议有「两个角色」,

A节点是事务的协调者,B和C是事务的参与者。

事务的提交分成两个阶段

第一个阶段是「投票阶段」

1.协调者首先将命令「写入日志」

2. 「发一个prepare命令」给B和C节点这两个参与者

3.B和C收到消息后,根据自己的实际情况,「判断自己的实际情况是否可以提交」

4.将处理结果「记录到日志」系统

5.将结果「返回」给协调者

springcloud分布式事务框架 springcloud分布式事务实现_分布式事务_05

第二个阶段是「决定阶段」

当A节点收到B和C参与者所有的确认消息后

  • 「判断」所有协调者「是否都可以提交」
  • 如果可以则「写入日志」并且发起commit命令
  • 有一个不可以则「写入日志」并且发起abort命令
  • 参与者收到协调者发起的命令,「执行命令」
  • 将执行命令及结果「写入日志」
  • 「返回结果」给协调者

  可能会存在哪些问题?

  • 「单点故障」:一旦事务管理器出现故障,整个系统不可用
  • 「数据不一致」:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 「响应时间较长」:整个消息链路是串行的,要等待响应结果,不适合高并发的场景
  • 「不确定性」:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

  三阶段提交(3PC)

三阶段提交又称3PC,相对于2PC来说增加了CanCommit阶段和超时机制。如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?

  • 第一阶段:「CanCommit阶段」这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。
  • 如果都返回yes,则进入第二阶段
  • 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
  • 第二阶段:「PreCommit阶段」此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
  • 第三阶段:「DoCommit阶段」在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

  补偿事务(TCC)

TCC其实就是采用的补偿机制,其核心思想是:「针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作」。它分为三个阶段:

「Try,Confirm,Cancel」

  • Try阶段主要是对「业务系统做检测及资源预留」,其主要分为两个阶段
  • Confirm 阶段主要是对「业务系统做确认提交」,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,「预留资源释放」。

比如下一个订单减一个库存:

springcloud分布式事务框架 springcloud分布式事务实现_协调者_06

执行流程:

  • Try阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,
  • 如果Try阶段「执行成功」,执行Confirm阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量
  • 如果Try阶段「执行失败」,执行Cancel阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量

TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点:

  • 1.「解决了协调者单点」,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 2.「同步阻塞」:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 3.「数据一致性」,有了补偿机制之后,由业务活动管理器控制一致性

总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的「增加」了业务代码的「复杂度」,因此,这种模式并不能很好地被复用。

  本地消息表

springcloud分布式事务框架 springcloud分布式事务实现_协调者_07

执行流程:

  • 消息生产方,需要额外建一个消息表,并「记录消息发送状态」。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。
  • 如果消息发送失败,会进行重试发送。
  • 消息消费方,需要「处理」这个「消息」,并完成自己的业务逻辑。
  • 如果是「业务上面的失败」,可以给生产方「发送一个业务补偿消息」,通知生产方进行回滚等操作。
  • 此时如果本地事务处理成功,表明已经处理成功了
  • 如果处理失败,那么就会重试执行。
  • 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

  消息事务

消息事务的原理是将两个事务「通过消息中间件进行异步解耦」,和上述的本地消息表有点类似,但是是通过消息中间件的机制去做的,其本质就是'将本地消息表封装到了消息中间件中'。

执行流程:

  • 发送prepare消息到消息中间件
  • 发送成功后,执行本地事务
  • 如果事务执行成功,则commit,消息中间件将消息下发至消费端
  • 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
  • 消费端接收到消息进行消费,如果消费失败,则不断重试

这种方案也是实现了「最终一致性」,对比本地消息表实现方案,不需要再建消息表,「不再依赖本地数据库事务」了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的「只有阿里的 RocketMQ」。

  最大努力通知

最大努力通知的方案实现比较简单,适用于一些最终一致性要求较低的业务。

执行流程:

  • 系统 A 本地事务执行完之后,发送个消息到 MQ;
  • 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
  • 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。

  Sagas 事务模型

Saga事务模型又叫做长时间运行的事务

其核心思想是「将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作」。

Seata框架中一个分布式事务包含3种角色:

「Transaction Coordinator (TC)」:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。「Transaction Manager (TM)」:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。「Resource Manager (RM)」:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

seata框架「为每一个RM维护了一张UNDO_LOG表」,其中保存了每一次本地事务的回滚数据。

具体流程:

1.首先TM 向 TC 申请「开启一个全局事务」,全局事务「创建」成功并生成一个「全局唯一的 XID」。

2.XID 在微服务调用链路的上下文中传播。

3.RM 开始执行这个分支事务,RM首先解析这条SQL语句,「生成对应的UNDO_LOG记录」。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。

4.RM在同一个本地事务中「执行业务SQL和UNDO_LOG数据的插入」。在提交这个本地事务前,RM会向TC「申请关于这条记录的全局锁」。

如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。

6.RM在事务提交前,「申请到了相关记录的全局锁」,然后直接提交本地事务,并向TC「汇报本地事务执行成功」。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。

7.TC根据所有的分支事务执行结果,向RM「下发提交或回滚」命令。

  • RM如果「收到TC的提交命令」,首先「立即释放」相关记录的全局「锁」,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
  • RM如果「收到TC的回滚命令」,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,
  • 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
    如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。

 

总结

本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解。

分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择,但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,「代码量上去了,业务复杂了,性能下跌了」。

所以,当我们真实开发的过程中,能不使用分布式事务就不使用。