写点什么

到底怎么理解分布式事务

  • 2023-02-20
    湖南
  • 本文字数:7335 字

    阅读完需:约 24 分钟

我们在过去总是使用本地事务,也就是数据库提供的事务操作,其中具有 ACID 的特性,但在如今我们的各个模块儿被拆分,服务与服务间相互调用,简单来说就是需要跨进程的事务,我们来想一下现有的本地事务是否能解决分布式事务。


情况 1:


用户直接调用订单模块儿,开启事务,然后在订单表中存入数据,然后再远程调用物流模块儿,去操作物流模块儿,我们可以想到如果物流模块儿出现问题,订单模块儿远程调用发生错误,是会进行事务回滚的。应该是没问题的。那如果物流模块儿确实修改成功了,但网络传输出现了问题,订单模块儿就进行回滚了,则就导致了物流模块儿有数据,订单模块儿没数据的问题。我们再考虑下面的问题:

情况二:

情况三:

前提理论

CAP 理论

CAP 表示一致性,可用性,分区容忍性。下面我们用数据库读写分离来演示


整体执行流程如下:

1、商品服务向主数据库写入商品信息(添加商品、修改商品、删除商品)

2、主数据库向商品服务响应写入成功。

3、商品服务请求从数据库读取商品信息。

一致性

表示写操作如果成功,各个节点上的读操作,应能读到最新的数据。


那么应如何保证?

  1. 在写入主数据库后应立刻将数据同步到从数据库中

  2. 在同步期间应对从数据库加锁,以防止读取到过期的数据


那这就存在一定的问题:比如加锁后的性能损耗。

可用性

指的是任何事务操作都能得到响应结果,不会出现响应超时,响应错误的问题。也就是说我允许我读到之前的数据,但不允许接收不到数据


如何保证?

  1. 数据还是应从主数据库同步到从数据库

  2. 不能将资源锁定

  3. 可以返回旧数据,甚至是默认数据,但不能返回错误数据或响应超时

分区容忍性

在微服务中数据分布到各个节点中,在网络分区中,允许因为网络问题导致的节点通信失败,但该节点应能继续对外提供服务,举例:1. 主数据库向从数据库同步失败,不能影响节点的读写操作 2. 其中一个节点挂了不能影响另一个。


如何操作

  1. 尽量使用异步操作同步数据,让节点松耦合

  2. 多添加从节点,保证备份节点


分区容忍性应该是分布式下最重要的特性。

在 CAP 理论中,是否三个特性能同时满足?


不能,在分区容忍性必备的情况下,一致性与可用性间存在矛盾,所以应看情况保证 CP 或者 AP 的特性。

AP 特性:放弃一致性,比如订单退款,并不是瞬间让钱到账,而是允许二十四小时内到账。


CP 特性:保证一致性,数据必须同步到最新状态,比如跨行转账,必须双方都完后事务才算完成。


一般情况下,使用的是 AP 特性,保证服务的可用。

BASE 理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障,允许部分功能不可用,但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终要达到一致状态。满足 BASE 理论的事务,我们称之为“柔性事务”。

  1. 基本可用:分布式系统中允许损失部分可用功能,来保证核心功能的可用

  2. 软状态:因为不要求强一致性,所以允许存在一个中间状态,比如订单的支付中,数据同步中等状态,最后改为成功状态。

  3. 最终一致性:就是从中间状态最终一定达到数据一致。

解决方案

  • 分布式事务解决方案---2PC(两阶段提交)


2PC 也称为两阶段提交,顾名思义是将事务分为两个阶段,准备阶段(Prepare),提交阶段(Commit)。举个栗子:

张三和李四好久不见,老友约起聚餐,饭店老板要求先买单,才能出票。这时张三和李四分别抱怨近况不如 意,囊中羞涩,都不愿意请客,这时只能 AA。只有张三和李四都付款,老板才能出票安排就餐。但由于张三和李四 都是铁公鸡,形成了尴尬的一幕:

准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。 提交阶段:老板出票,两人拿票纷纷落座就餐。


例子中形成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,并且会把已收款退回。


整个事务过程由事务管理器和参与者组成,店老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。


而在一些关系型数据库中(Oracle,Mysql)都是支持两阶段提交协议

  1. 准备阶段(Prepare phase):事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件),这时候资源是被锁定的。

  2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。


成功情况:

失败情况:


为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型 DTP(Distributed Transaction Processing Reference Model)。

XA 方案

DTP 模型定义如下角色: AP(Application Program):即应用程序,可以理解为使用 DTP 分布式事务的程序。 RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。 TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个 RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。 DTP 模型定义 TM 和 RM 之间通讯的接口规范叫 XA,简单理解为数据库提供的 2PC 接口协议,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案。 以上三个角色之间的交互方式如下:

TM 向 AP 提供 应用程序编程接口,AP 通过 TM 提交及回滚事务。 TM 交易中间件通过 XA 接口来通知 RM 数据库事务的开始、结束以及提交、回滚等。

总结: 整个 2PC 的事务流程涉及到三个角色 AP、RM、TM。AP 指的是使用 2PC 分布式事务的应用程序;RM 指的是资源管理器,它控制着分支事务;TM 指的是事务管理器,它控制着整个全局事务。

XA 方案的问题:

  1. 需要数据库支持 XA 协议

  2. 资源锁需要等两个阶段结束才释放,性能差


Seata

阿里开源的分布式事务框架,传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT(2PC)、TCC、SAGA 和 XA 事务模式。


第一阶段就将本地事务提交了(提交前需要获取全局事务锁),全局事务管理器统计所有分支事务的结果,如果某个分支出现了异常,在本地事务中有一个 UNDO_LOG Table 的数据表,记录了之前的数据,修改 SQL,修改后的数据。可通过它进行事务的回滚,这里不会产生脏数据就是因为本地事务的提交需要先获取全局事务锁。


第二阶段,如果分支事务都成功,则根据事务 ID 删除 undo_log 的记录,如果失败,则找到记录进行回滚。 这里业务说明官网已经写的很详细了,这里我不再赘述。 Seata 的核心就是通过 undo log 文件让已经提交的事务,仍能回滚。


小结:

这里我们介绍了传统的 2PC(基于数据库 XA 协议)和 Seata 实现 2PC 的方案。 Seata 实现的要点:

  1. 全局事务开始使用 @GlobalTransactional 标识 。

  2. 每个本地事务方案仍然使用 @Transactional 标识。

  3. 每个数据都需要创建 undo_log 表,此表是 seata 保证本地事务一致性的关键。

  4. TM 获取到的 XID 会通过远程调用时传入。

分布式事务解决方案---TCC

TCC 与 2PC 有啥区别? 为啥使用?


2PC 是定义在数据层的,而且有全局锁的存在,也会有一定的性能消耗。TCC 是在业务层定义的,更加灵活,但复杂度也会上升,TCC 是 try,conform,console 三个方法的简写。也就是想实现分布式事务,需要实现这三个方法。下面我们使用框架进行演示

hmily

TCC 需要注意三种异常处理分别是空回滚、幂等、悬挂:** **


这里是指 TCC 中的 try,conform,console 是三个独立的线程去完成的,且分布式调用具体网络延迟的可能性。

  1. 空回滚:try 还没执行时,执行 console。解决思路:我们记录 TM 生成的全局事务 ID,来判断 try 是否已经执行,如果未执行则不执行 console。

  2. 悬挂:conform 或 console 已经执行了,才开始执行 try 解决思路:在 conform 和 console 执行时将执行记录插入数据,当执行 try 时进行判断

  3. 幂等性:conform 和 console 都是默认成功的,当执行失败时,会不断重试,这就需要保证代码代码幂等性解决思路:执行前,先通过全局唯一事务 ID,查看自己之前是否执行过。

这里有一个拓宽的思路,就是在本地事务中,记录一张记录表,来判断操作是否执行过,由于是在同一本地事务内,所以可以保证该记录的准确性。


Try,Console,Conform 需要严格处理上面的三个问题,下面我们用业务来说明:场景为 A 转账 30 元给 B,A 和 B 账户在不同的服务。

// 账户A try:检查余额是否够30元 扣减30元  (操作是直接提交本地事务的)
confirm: 空
cancel:增加30元
// 账户Btry:增加30元
confirm: 空
cancel:减少30元
复制代码
存在问题:

1)如果账户 A 的 try 没有执行,执行 cancel 则就多加了 30 元。 2)由于 try,cancel、confifirm 都是由单独的线程去调用,且会出现重复调用,所以都需要实现幂等。 3)账号 B 在 try 中增加 30 元,当 try 执行完成后可能会其它线程给消费了。 4)如果账户 B 的 try 没有执行在 cancel 则就多减了 30 元。

问题解决:

1)账户 A 的 cancel 方法需要判断 try 方法是否执行,正常执行 try 后方可执行 cancel。 2)try,cancel、confifirm 方法实现幂等。 3)账号 B 在 try 方法中不允许更新账户金额,在 confirm 中更新账户金额。 4)账户 B 的 cancel 方法需要判断 try 方法是否执行,正常执行 try 后方可执行 cancel。

// 账户Atry:  try幂等校验,判断之前执行过没  try悬挂处理,判断conform和cancel执行过没  检查余额是否够30元  扣减30元 
confirm: 空
cancel: cancel幂等校验 cancel空回滚处理 增加可用余额30元
// 账户Btry:空
confirm: confirm幂等校验 正式增加30元
cancel:空
复制代码

分布式事务解决方案---可靠消息最终一致性

也算是 Base 理论的实现 可靠消息:张三给李四发钱,张三的账户先减少钱,然后发送一条消息到消息队列,李四进行接收消息增加钱,保证整个过程的可靠性。 最终一致性:张三发钱后,是不能再回滚的,李四就必须获取并消费消息,保证的最终数据是一致的。


这中间有什么问题?

张三向消息队列发送消息就一定会出现网络问题,比如:事务内,张三钱减少了,然后发送数据到消息队列,如果发送错误张三的钱也可以回滚,但是可能消息已经到队列了,但返回时网络延迟发生错误,导致数据回滚,则队列中数据是仍然存在的,所以一定要保证数据操作与消息发送保证原子性。 李四需要必须接收并完成消息,但可能出现问题导致不断的重试,所以有幂等性的问题

RocketMq


这是大概的一个流程,我们现在分析可能存在哪些问题: 张三向消息队列成功发送事务消息后,有一个 rockmq 监听本地事务方法的返回。这时候事务消息并不能被消费,如果张三正常提交本地事务,则该事务消息可被消费,如果本地事务没有提交成功,返回 Rollback 则事务消息删除,如果返回 UnKnown(比如出现某些异常),则过一段时间,消息队列会再次监听本地事务执行情况。从而保证张三本地事务与事务消息的原子性。 李四从消息队列获取消息后,应该进行本地事务并确认该事务消息,如果没有确认,消息队列会不断发给消费者,最终也可人工处理。 可靠消息最终一致性的优点就是如果李四那边要执行很久,张三这边是不用等的,只要最终确认该事务消息即可。进行了异步解耦。

分布式事务解决方案---最大努力通知

RocketMq

我们以支付业务为例这里的账户系统就是等待通知的 充值系统当充值完毕后发起通知

这里我们看出最大努力通知的目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。


具体包括:

1.有一定的消息重复通知机制

因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知


2.消息校验机制

如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询信息来满足需求。


最大努力通知与可靠消息一致性有什么不同?


1.解决思想不同

可靠消息一致性主要保证消息发送方保证把消息发送出去,并消息接收方成功获取并消费该消息,关键在发起方。 最大努力通知则侧重消息能被消息接受者收到,或者可以消息接收方主动来查询消息状态,关键在于接收方。


2.业务应用场景不同

可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。


3.技术解决方向不同

可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

解决方案:

消息接收方直接监听消息队列


由消息通知系统监听消息队列,然后再通知消息接收方


这两者有啥不同?

第一种只适用于内部网络系统,也就是发起方和接收方都归你管理。但如果说消息发送方是第三方系统,人家就不能让你监听人家的消息队列吧,只能说加入一个中间通知系统,然后通过网络请求来通知你。

分布式事务综合案例分析

在上面我们已经学习了四种不同的分布式事务解决方案,我们需要进行区分,哪种业务场景需要哪种实现,各个方案有什么区别。

场景一


用户向用户中心发起注册请求,用户中心保存用户业务信息,然后通知统一账号服务新建该用户所对应登录账号。 针对注册业务,如果用户与账号信息不一致,则会导致严重问题,因此该业务对一致性要求较为严格,即当用户服务和账号服务任意一方出现问题都需要回滚事务


业务分析

1.使用最大努力通知

不可以,用户服务注册好后,通知账户服务,这里只能说不断通知,没法做到回滚

2.使用可靠消息一致性

不可以,用户发送事务消息,然后进行本地事务,如果成功了,也无法保证账户服务成功消费,无法回滚

3.TCC 方案

可行,支持事务回滚,性能好,但实现复杂

4.2PC 方案

可行,支持事务回滚,有性能损耗,但简单

场景二


用户向用户中心提交开户资料,用户中心生成开户请求号并重定向至银行存管系统开户页面。用户设置存管密码并确认开户后,银行存管立即返回“请求已受理”。在某一时刻,银行存管系统处理完该开户请求后,将调用回调地址通知处理结果,若通知失败,则按一定策略重试通知。同时,银行存管系统应提供开户结果查询的接口,供用户中心校对结果。


业务分析

1.Seata 方案

不可行,银行存管系统不能让你去编写业务代码吧

2.TCC 方案

不可行,银行存管系统不会让你写 try,conform,console 吧

3.可靠消息最终一致性

不可行,银行系统不会和用户系统直接通过 MQ 交互

4.最大努力通知

可行,我们发送请求给银行系统,银行系统最终给我返回结果,或者我们也可主动查询

场景三


管理员对某标的满标审批通过,交易中心修改标的状态为“还款中”,同时要通知还款服务生成还款计划。

业务分析

1.使用 Seata

不行,生成计划如果过久,Seata 锁定资源

2.使用 TCC

本需求对业务一致性要求较低,因为生成还款计划的时长较长,所以不要求交易中心修改标的状态为“还款中”就立 即生成还款计划 ,所以本方案不适用。

3.使用努力通知

满标审批通过后由交易中心向还款服务发送通知要求生成还款计划,还款服务并且对外提供还款计划生成结果校对接口供其它服务查询,最大努力 通知方案也适用本场景 。

4.使用消息一致性

满标审批通过后由交易中心修改标的状态为“还款中”并且向还款服务发送消息,还款服务接收到消息开始生成还款计划,基本于 MQ 的可靠消息一致性方案适用此场景 。

总结

分布式事务对比分析2PC 最大的诟病是一个阻塞协议。RM 在执行分支事务后需要等待 TM 的决定,此时服务会阻塞并锁定资源。由于其 阻塞机制和最差时间复杂度高, 因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并 发较高以及子事务生命周期较长 (long-running transactions) 的分布式服务中。


如果拿 TCC 事务的处理流程与 2PC 两阶段提交做比较,2PC 通常都是在跨库的 DB 层面,而 TCC 则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实 现不同的回滚策略。典型的使用场景:满,登录送优惠券等。


可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消 息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注 册送积分,登录送优惠券等。


最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。


总结: 在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?


分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务 ACID 做对比。无论是数据库层的 XA、还是应用层 TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。


作者:啵啵肠

链接:https://juejin.cn/post/7172094503091142663

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
到底怎么理解分布式事务_做梦都在改BUG_InfoQ写作社区