好好学习

扫一扫关注

为什么不建议你用分布式事务?

下载文本     天下第一大好人2022-09-28 12:40:04 34000

【51CTO.com原创稿件】伴随着业务的快速的发展、越来越高的业务复杂度,几乎每个公司的系统都会从单体走向分布式,特别是转向微服务架构。

 

图片来自 包图网

随之而来就必然遇到分布式事务这个难题。而我的这篇文章总结了分布式事务的解决方案,希望给大家带来帮助。

分布式事务基础①到底什么是事务呢?

什么是事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。

明白上述例子,再来看事务的定义:事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

②先来回顾本地事务

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务。

由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。

回顾一下数据库事务的四大特性 ACID:

A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。 C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。

比如:张三向李四转 100 元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出 100 元,李四账户没有增加 100 元这就出现了数据错误,就没有达到一致性。

I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。 D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。

数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

③分布式事务

银行跨行转账业务是一个典型分布式事务场景,假设 A 需要跨行转账给 B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的 ACID,只能够通过分布式事务来解决。

分布式事务就是指事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。

在上述转账的业务中,用户 A-100 操作和用户 B+100 操作不是位于同一个节点上。

本质上来说,分布式事务就是为了保证在分布式场景下,数据操作的正确执行。

分布式事务在分布式环境下,为了满足可用性、性能与降级服务的需要,降低一致性与隔离性的要求,一方面遵循 base 理论(base 相关理论,涉及内容非常多,感兴趣的程序员们,可以参考 base 理论)。

base 理论:

基本业务可用性(Basic Availability) 柔性状态(Soft state) 最终一致性(Eventual consistency)

同样的,分布式事务也部分遵循 ACID 规范:

原子性:严格遵循 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽 持久性:严格遵循④分布式事务场景

典型的场景就是微服务架构:微服务之间通过远程调用完成事务操作。

比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。简言之:跨 JVM 进程产生分布式事务。

单体系统访问多个数据库实例:当单体系统需要访问多个数据库(实例)时就会产生分布式事务。

比如:用户信息和订单信息分别在两个 MySQL 实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。

简言之:跨数据库实例产生分布式事务。

多服务访问同一个数据库实例:比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨 JVM 进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。

分布式事务的解决方案①2PC(两阶段提交)/XA

2PC(Two-phase commit protocol),中文叫二阶段提交。

二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。

XA 是由 X/Open 组织提出的分布式事务的规范,XA 规范主要定义了(全局)事务管理器(TM)和(局部)资源管理器(RM)之间的接口。本地的数据库如 MySQL 在 XA 中扮演的是 RM 角色。

XA 一共分为两阶段:

第一阶段(prepare):即所有的参与者 RM 准备执行事务并锁住需要的资源。参与者 ready 时,向 TM 报告已准备就绪。 第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都 ready 后,向所有参与者发送 commit 命令。

目前主流的数据库基本都支持 XA 事务,包括 MySQL、Oracle、SQL Server、Postgre。

XA 事务由一个或多个资源管理器(RM)、一个事务管理器(TM)和一个应用程序(ApplicationProgram)组成。

如果有任何一个参与者 prepare 失败,那么 TM 会通知所有完成 prepare 的参与者进行回滚。

XA 事务的特点是:

简单易理解,开发较容易 对资源进行了长时间的锁定,并发度低②三阶段提交(3PC)

三阶段提交又称 3PC,相对于 2PC 来说增加了 CanCommit 阶段和超时机制。

如果段时间内没有收到协调者的 commit 请求,那么就会自动进行 commit,解决了 2PC 单点故障的问题。但是性能问题和不一致问题仍然没有根本解决。

下面我们还是一起看下三阶段流程的是什么样的?

第一阶段:CanCommit 阶段。这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。

如果都返回 yes,则进入第二阶段。有一个返回 no 或等待响应超时,则中断事务,并向所有参与者发送 abort 请求。

第二阶段:PreCommit 阶段。此时协调者会向所有的参与者发送 PreCommit 请求,参与者收到后开始执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。

参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。

第三阶段:DoCommit 阶段。在阶段二中如果所有的参与者节点都可以进行 PreCommit 提交,那么协调者就会从“预提交状态”转变为“提交状态”。

然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的 Ack 消息后完成事务。

相反,如果有一个参与者节点未完成 PreCommit 的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送 abort 请求,从而中断事务。

③SAGA

Saga 是这一篇数据库论文 saga 提到的一个方案。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

把上面的转账作为例子,一个成功完成的 SAGA 事务时序图如下:

SAGA 事务的特点:

并发度高,不用像 XA 事务那样长期锁定资源 需要定义正常操作以及补偿操作,开发量比 XA 大 一致性较弱,对于转账,可能发生 A 用户已扣款,最后转账又失败的情况④TCC

2PC 是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!

TCC 的 3 个阶段:

Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性) Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作要求具备幂等设计,Confirm 失败后需要进行重试。 Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,也可以理解为可以理解为把预留阶段的动作撤销了。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致,要求满足幂等设计。

把上面的转账作为例子,通常会在 Try 里面冻结金额,但不扣款,Confirm 里面扣款,Cancel 里面解冻金额。

一个成功完成的 TCC 事务时序图如下:

TCC 特点如下:

并发度较高,无长期资源锁定 开发量较大,需要提供 Try///confirm/i/i/Cancel 接口 一致性较好,不会发生 SAGA 已扣款最后又转账失败的情况 TCC 适用于订单类业务,对中间状态有约束的业务⑤本地消息表

本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。

本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。

如果调用失败也没事,会有后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。

这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。

可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。

⑥消息事务

RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。

第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。

并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。

如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。

如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。

同时也可以看到消息事务实现的也是最终一致性。

⑦最大努力通知

发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。

具体包括:

有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

前面介绍的的本地消息表和消息事务都属于可靠消息,与这里介绍的最大努力通知有什么不同?

可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。

最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。

解决方案上,最大努力通知需要:

提供接口,让接受通知放能够通过接口查询业务处理结果 消息队列 ACK 机制,消息队列按照间隔 1min、5min、10min、30min、1h、2h、5h、10h 的方式,逐步拉大通知间隔 ,直到达到通知要求的时间窗口上限。之后不再通知

最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。

⑧AT 事务模式

这是阿里开源项目 seata 中的一种事务模式,在蚂蚁金服也被称为 FMT。优点是该事务模式使用方式,类似 XA 模式,业务无需编写各类补偿操作,回滚由框架自动完成,缺点也类似 AT,存在较长时间的锁,不满足高并发的场景。

在 Seata 项目中,最早由阿里巴巴中间件开源出的 AT 模式(Automatic Transaction) 是一套创新的、业务无侵入的分布式事务解决方案。

截止 Seata 的 GA 版本发布,AT 模式 已经在开源社区引起了广泛关注,很多家企业用户已经将 Seata 的 AT 模式应用于生产。

AT 模式一阶段:首先,在 Seata 的组件中,如果你想开启分布式事务,那么就应该在你的业务入口或者事务发起入口加上 @GlobalTransactional 注解。

如果你是 AT 模式就要做好数据源代理(seata1.0 后全面支持自动代理),并被 sqlsessionfactroy 使用(或者直接 jdbc 操作使用被代理数据源)。

可以发现比较关键的异步,与其他模式的区别便是代理数据源,而代理数据源又有什么奥秘呢?

AT 模式二阶段提交:二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

AT 模式二阶段回滚:二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”。

如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。

小结

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

分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择。

但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,「代码量上去了,业务复杂了,性能下跌了」。

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

作者:JackHu

简介:水滴健康基础架构资深技术专家

编辑:陶家龙

征稿:有投稿、寻求报道意向技术人请联络 [email protected]

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

 

 
反对 0举报 0 收藏 0 评论 0

(c)2022 haohaoxuexi.cc SYSTEM All Rights Reserved

冀ICP备17031443号-5