天天看点

一篇文章带你理解分布式事务

作者:尚硅谷教育

1 什么是事务

不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。

再比如,微信转钱。你给你女朋友转5000块钱,当你微信转钱时,发现网络错误,此时系统提示网络错误,你的钱已经从你的卡里扣除了,但是你女朋友账户却没收到任何转账。

又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的。这说明了事务在生活中的一些重要性。

有了事务,你去小卖铺买东西,那就是一手交钱一手交货。你给你女朋友转账,要不就是你款扣5000,女朋友账户多5000。否则就是你钱没扣,女朋友那里也不会出现多收款情况。有了事务,你去网上购物,扣款即产生订单交易。

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。

2 事务的四大特性

A:原子性(Atomicity),一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

就像你买东西要么交钱收货一起都执行,要么发不出货,就退钱。

C:一致性(Consistency),事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation),指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其他人的。

D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。

即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

3 本地事务

什么是本地事务(Local Transaction)?本地事务也称为数据库事务或传统事务(相对于分布式事务而言)。尤其对于数据库而言,为了数据安全,提供了以下的几个步骤来完成本地事务的提交以及回滚

1. transaction begin

2. insert/delete/update

3. insert/delete/update

4. 多个写操作...

5. transaction commit/rollback

一篇文章带你理解分布式事务
一篇文章带你理解分布式事务

在单体应用下,我们开发的业务逻辑比较复杂。尤其某些业务之间存在强依赖性、强耦合性,数据需要保持特别严苛的强一致性。比如购买功能:我们购买对应的商品,一部分需要扣除我们的余额,余额扣减后商家就安排相应的发货。如果不使用事务,比如第一步扣减余额出现问题,后面商家就直接发货了。再或者余额扣了,商家发货执行失败。无论哪种情况出现,都会造成巨大的缺陷,要不就是客户付钱不发货,要不就是客户不付钱却收到了货。这两种无论出现哪种问题后果都将会是致命的。所以我们需要事务控制,把两者作为一个整体,要不两者同时成功,要不全部失败;不可能出现一个成功,一个失败的情况。

如下面代码逻辑所示

1. transaction begin

try{

2.调用账户模块扣减余额,(数据库更新操作)

3. 判断余额是否扣减成功

4. 余额扣减成功,商品发货(更新数据库)

5.两者都成功, transaction commit

}catch(Exception e){

6. transaction rollback

}finally{

7.关闭资源

}

4 什么是分布式事务

事务涉及多个操作,多个操作之间的数据必须保持强一致性。分布式事务是指组成事务的参与者,每个业务部分都分别部署在不同的服务器上。在微服务架构中多个节点的协调工作必须保持原子性,多个节点的逻辑必须同时成功或者同时失败。不能出现部分节点成功,部分失败的情况。一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

本质上来说,分布式事务就是为了保证不同数据库、不同服务器节点的数据一致性。

一篇文章带你理解分布式事务

4.1 库存订单案例

现在有一个库存服务,部署在A机器,有一个订单微服务,部署在B机器。客户此时在客户端发起一次订单请求,需要两个微服务协调工作。后台根据请求,首先将请求路由到库存服务更新商品的库存,库存扣减完毕后再调用订单服务创建订单。

一篇文章带你理解分布式事务

正常情况下,两个数据库各自更新成功,两边数据维持着一致性。

一篇文章带你理解分布式事务

但是,在非正常情况下,比如订单服务崩溃了,请求超时。导致库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。

一篇文章带你理解分布式事务

这时候,我们并不能再像单体服务那样调用本地事务解决,因为组成事务的每个步骤现在都在不同的服务器节点上。通常对于分布式的数据安全问题,我们主要有以下几点解决方案:

4.2 分布式事务解决方案

4.2.1 基于XA协议的两阶段提交(2PC)

XA协议:XA是一个分布式事务协议。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

4.2.1.1 思路

2PC顾名思义分为两个阶段,其实施思路可概括为:

(1)投票阶段(voting phase):参与者将操作结果通知协调者;

(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;

4.2.1.2 举例

ABCDE五个室友,A组织一场王者荣耀开黑游戏,A需要拉其他四个室友五排,为了大家都有时间,你需要发送信息去问室友。这时候A就属于协调者,BCDE属于参与者、

投票阶段:

(1)A在寝室群发送一条消息,说今晚下课后寝室五黑,询问室友是否有时间;

(2)B回复有时间;

(3)C回复有时间;

(4)D回复有时间

(5)E迟迟不回复,此时对于这个活动,ABCD均处于阻塞状态,算法无法继续进行;

提交阶段:

(1)协调者A将收集到的结果反馈给BCDE(什么时候反馈,以及反馈结果如何,在此例中取决与E的时间与决定);

(2)B收到;

(3)C收到;

(4)D收到;

(5)E收到;

4.2.1.3 实际应用交互流程

第一阶段:2PC中包含着两个角色:事务协调者和事务参与者。让我们来看一看他们之间的交互流程:

一篇文章带你理解分布式事务

在分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。

在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。

当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。

第二阶段:

一篇文章带你理解分布式事务

在2PC分布式事务的第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。

接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。

当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。

以上所描述的是2PC两阶段提交的正向流程,接下来我们看一看失败情况的处理流程

第一阶段

一篇文章带你理解分布式事务

第二阶段

一篇文章带你理解分布式事务

在2PC的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。

于是在第二阶段,事务协调节点向所有的事务参与者发送Abort(中止)请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。

以上就是2PC两阶段提交协议的详细过程。

4.2.1.4 不足点

1.性能问题

2PC遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。

2.协调者单点故障问题

事务协调者是整个2PC模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。

3.丢失消息导致的不一致问题。

在2PC协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

4.2.2 代码补偿事务(TCC)

TCC的作用主要是解决跨服务调用场景下的分布式事务问题

4.2.2.1场景案例

以航班预定的案例,来介绍TCC要解决的事务场景。在这里虚构一个场景,把自己当做航班预定的主人公,来介绍这个案例。从合肥 –> 昆明 –> 大理。

准备从合肥出发,到云南大理去游玩,然后使用美团App(机票代理商)来订机票。发现没有从合肥直达大理的航班,需要到昆明进行中转。如下图:

一篇文章带你理解分布式事务

从图中我们可以看出来,从合肥到昆明乘坐的是四川航空,从昆明到大理乘坐的是东方航空。

由于使用的是美团App预定,当我选择了这种航班预定方案后,美团App要去四川航空和东方航空各帮我购买一张票。如下图:

一篇文章带你理解分布式事务

考虑最简单的情况:美团先去川航帮我买票,如果买不到,那么东航也没必要买了。如果川航购买成功,再去东航购买另一张票。

现在问题来了:假设美团先从川航成功买到了票,然后去东航买票的时候,因为天气问题,东航航班被取消了。那么此时,美团必须取消川航的票,因为只有一张票是没用的,不取消就是浪费我的钱。那么如果取消会怎样呢?如果读者有取消机票经历的话,非正常退票,肯定要扣手续费的。在这里,川航本来已经购买成功,现在因为东航的原因要退川航的票,川航应该是要扣代理商的钱的。

那么美团就要保证,如果任一航班购买失败,都不能扣钱,怎么做呢?

两个航空公司都为美团提供以下3个接口:机票预留接口、确认接口、取消接口。美团App分2个阶段进行调用,如下所示:

一篇文章带你理解分布式事务

在第1阶段:

美团分别请求两个航空公司预留机票,两个航空公司分别告诉美团预留成功还是失败。航空公司需要保证,机票预留成功的话,之后一定能购买到。

在第2阶段:

如果两个航空公司都预留成功,则分别向两个公司发送确认购买请求。

如果两个航空公司任意一个预留失败,则对于预留成功的航空公司也要取消预留。这种情况下,对于之前预留成功机票的航班取消,也不会扣用户的钱,因为购买并没实际发生,之前只是请求预留机票而已。

通过这种方案,可以保证两个航空公司购买机票的一致性,要不都成功,要不都失败,即使失败也不会扣用户的钱。如果在两个航班都已经已经确认购买后,再退票,那肯定还是要扣钱的。

当然,实际情况肯定这里提到的肯定要复杂,通常航空公司在第一阶段,对于预留的机票,会要求在指定的时间必须确认购买(支付成功),如果没有及时确认购买,会自动取消。假设川航要求10分钟内支付成功,东航要求30分钟内支付成功。以较短的时间算,如果用户在10分钟内支付成功的话,那么美团会向两个航空公司都发送确认购买的请求,如果超过10分钟(以较短的时间为准),那么就不能进行支付。

这个方案提供给我们一种跨服务保证事务一致性的一种解决思路,可以把这种方案当做TCC的雏形。

TCC是Try ( 尝试 ) — Confirm(确认) — Cancel ( 取消 ) 的简称:

一篇文章带你理解分布式事务

有哥们立马会想到,TCC与XA两阶段提交有着异曲同工之妙,下图列出了二者之间的对比:

一篇文章带你理解分布式事务

1) 在阶段1:

在XA中,各个RM准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);而在TCC中,是主业务活动请求(try)各个从业务服务预留资源。

2) 在阶段2:

XA根据第一阶段每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。

TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求。

一篇文章带你理解分布式事务

其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务

一篇文章带你理解分布式事务

例如:A要向 B 转账,思路大概是

假设用户user表中有两个字段:可用余额(available_money)、冻结余额(frozen_money)

A扣钱对应服务A(ServiceA)

B加钱对应服务B(ServiceB)

转账订单服务(OrderService)

业务转账方法服务(BusinessService)

ServiceA,ServiceB,OrderService都需分别实现try(),confirm(),cancle()方法,方法对应业务逻辑如下

一篇文章带你理解分布式事务

其中业务调用方BusinessService中就需要调用ServiceA.try()ServiceB.try()OrderService.try()

1、当所有try()方法均执行成功时,对全局事物进行提交,即由事物管理器调用每个微服务的confirm()方法

2、 当任意一个方法try()失败(预留资源不足,抑或网络异常,代码异常等任何异常),由事物管理器调用每个微服务的cancle()方法对全局事务进行回滚

4.2.2.2 优点

跟2PC(很多第三方框架)比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

4.2.2.3 缺点

缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

4.2.3 Seata(理想方式)

seata是阿里开源的一个分布式事务框架,能够让大家在操作分布式事务时,像操作本地事务一样简单。一个注解搞定分布式事务。

解决分布式事务问题,有两个设计初衷

对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入高性能:减少分布式事务解决方案所带来的性能消耗

seata中有两种分布式事务实现方案,AT及TCC

  • AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题 2PC-改进
  • TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题

那 Seata 是怎么做到的呢?下面说说它的各个模块之间的关系。

Seata 的设计思路是将一个分布式事务可以理解成一个全局事务,下面挂了若干个分支事务,而一个分支事务是一个满足 ACID 的本地事务,因此我们可以操作分布式事务像操作本地事务一样。

2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。

4.2.3.1 AT模式

它使得应用代码可以像使用本地事务一样使用分布式事务,完全屏蔽了底层细节

AT 模式下,把每个数据库被当做是一个 Resource,Seata 里称为 DataSource Resource。业务通过 JDBC 标准接口访问数据库资源时,Seata 框架会对所有请求进行拦截,做一些操作。每个本地事务提交时,Seata RM(Resource Manager,资源管理器) 都会向 TC(Transaction Coordinator,事务协调器) 注册一个分支事务。当请求链路调用完成后,发起方通知 TC 提交或回滚分布式事务,进入二阶段调用流程。此时,TC 会根据之前注册的分支事务回调到对应参与者去执行对应资源的第二阶段。TC 是怎么找到分支事务与资源的对应关系呢?每个资源都有一个全局唯一的资源 ID,并且在初始化时用该 ID 向 TC 注册资源。在运行时,每个分支事务的注册都会带上其资源 ID。这样 TC 就能在二阶段调用时正确找到对应的资源。

一篇文章带你理解分布式事务

解释:

Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并决定全局事务的提交或回滚。Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。Resource Manager (RM):资源管理器,负责本地事务的注册,本地事务状态的汇报(投票),并且负责本地事务的提交和回滚。

XID:一个全局事务的唯一标识

其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:

一篇文章带你理解分布式事务

下面是一个分布式事务在Seata中的执行流程:

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  • XID 在微服务调用链路的上下文中传播。
  • RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC
  • TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

Seata 中有三大模块,分别是 TM、RM 和 TC。其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

4.2.3.2 MT 模式

Seata还支持MT模式。MT模式本质上是一种TCC方案,业务逻辑需要被拆分为 Prepare/Commit/Rollback 3 部分,形成一个 MT 分支,加入全局事务。如图所示:

一篇文章带你理解分布式事务

MT 模式一方面是 AT 模式的补充。另外,更重要的价值在于,通过 MT 模式可以把众多非事务性资源纳入全局事务的管理中。

5 总结

在企业开发中,数据安全问题一定是排在第一位的。随着分布式架构的流行,软件结构越来越复杂;数据的同步、如何保证数据的强一致性已经成为了一个越来越常见的问题。使用分布式事务,能够解决多个微服务协同工作时引发的数据不一致问题。所以我们的精力仅仅集中在开发业务逻辑是远远不够的,还需要为系统的数据安全建立起可靠的保障。一般能使用本地事务解决的问题尽量使用本地事务,因为架构越来越复杂,新的技术在解决问题的同时,同时也会引发新技术所带来的问题。我们在解决这些新问题的同时,意味着会引进更多的资源,消耗更多的性能,而且维护起来也会越来越复杂。无论是数据库层的XA、还是应用层TCC、最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。

继续阅读