分布式事务解决方案

101 阅读20分钟

分布式事务

分布式事务的定义: 分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点上

下面拿下单扣减库存、扣余额来举例: 当系统的体量很小时,单体架构完全可以满足现有业务需求,所有的业务共用一个数据库,整个下单流程或许只用在一个方法里同一个事务下操作数据库即可。此时做到所有操作要么全部提交 或 要么全部回滚很容易。

image.png

分库分表、SOA

可随着业务量的不断增长,单体架构渐渐扛不住巨大的流量,此时就需要对数据库、表做 分库分表处理,将应用 SOA 服务化拆分。也就产生了订单中心、用户中心、库存中心等,由此带来的问题就是业务间相互隔离,每个业务都维护着自己的数据库,数据的交换只能进行 RPC 调用。

当用户再次下单时,需同时对订单库 order、库存库 storage、用户库 account 进行操作,可此时我们只能保证自己本地的数据一致性,无法保证调用其他服务的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。

image.png

CAP理论

如何进行分布式事务控制?CAP理论是分布式事务处理的理论基础,了解了CAP理论有助于我们研究分布式事务的 处理方案。

CAP理论是:分布式系统在设计时只能在一致性(Consistency)可用性(Availability)分区容忍性(Partition Tolerance) 中满足两种,无法兼顾三种。

通过下图理解CAP理论:

image.png

一致性(Consistency): 服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致 性。

可用性(Availability): 服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结 点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。

分区容忍性(Partition Tolerance): 分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区 导致数据的不完整及无法访问等问题。

分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现 象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。

分布式系统能否兼顾C、A、P? 在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个结点,如果要保证数 据的一致性就要实现每个结点的数据一致,结点越多可用性越好,但是数据一致性越差。 所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。

CAP有哪些组合方式? 1、CA:放弃分区容忍性,加强一致性和可用性,关系数据库按照CA进行设计。 2、AP:放弃一致性,加强可用性和分区容忍性,追求最终一致性,很多NoSQL数据库按照AP进行设计。 说明:这里放弃一致性是指放弃强一致性,强一致性就是写入成功立刻要查询出最新数据。追求最终一致性是指允 许暂时的数据不一致,只要最终在用户接受的时间内数据 一致即可。 3、CP:放弃可用性,加强一致性和分区容忍性,一些强一致性要求的系统按CP进行设计,比如跨行转账,一次转 账请求要等待双方银行系统都完成整个事务才算完成。

说明:由于网络问题的存在CP系统可能会出现待等待超时,如果没有处理超时问题则整理系统会出现阻塞。

在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最 新据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的 时间内退款事务走完即可。

解决方案

2PC

基于XA协议实现的分布式事务,XA协议中分为两部分:事务管理者和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、Mysql这些数据库都实现了XA接口,而事务管理器则作为一个全局的调度者。

两阶段提交(2PC),对业务侵入很小,它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用基于XA协议的分布式事务,能够严格保障事务ACID特性。

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

注意这只是协议或者说是理论指导,只阐述了大方向,具体落地还是会有差异的。

两个阶段的具体流程:

准备阶段协调者会给各参与者发送准备命令,你可以把准备命令理解除了提交事务之外啥事都做完了。

同步等待所有资源的响应之后就进入第二节点即提交阶段(注意:提交阶段不一定是提交事务,也可能是回滚事务)。

假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务的指令。然后等待所有事务都提交成功之后,返回事务执行成功

看一下流程图:

image.png

假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的指令,即分布式事务执行失败。

image.png

如果第二阶段提交失败的话,怎么处理呢?

这里有两种情况: 第一种是第二阶段执行的是回滚事务操作, 那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。

第二种是第二阶段执行的是提交事务操作, 那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到事务提交成功,如果还是失败的话,那就只能是人工介入处理。

协调者故障分析

协调者是一个节点,存在单点故障问题。

假设协调者在发送准备命令之前挂掉,等于事务还没开始。

假设协调者在发送准备命令之后挂掉,准备成功的事务参与者都处于事务资源锁定的状态。不仅本地事务不会接收到提交指令,还会因为锁定了一些公共资源而阻塞系统其他操作。

假设协调者在发送回滚事务命令之前挂掉,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着,同上。

假设协调者在发送回滚事务命令之后挂掉,这个还可以,至少命令发出去了,很大的概率都会回滚成功,资源都会释放,但是如果网络分区问题,某些参与者将因为收不到命令而阻塞着。

假设协调者在发送提交事务命令之后挂了,同上

所以协调者肯定不能以单例的形式存在。

2PC是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

2PC具体的实现: 例如Tree 2PC, Dynamic 2PC。

2PC的优缺点

优点

  1. 数据一致性: 2PC协议确保了分布式系统中的一致性。在事务提交前,所有参与者都统一提交或回滚,从而避免了数据不一致的情况。
  2. 容错性: 2PC协议是一种容错机制,能够应对各种故障情况,包括参与者崩溃、网络故障等。它保证了即使在这些故障情况下,事务也能够达到一致的状态。
  3. 简单理解和实现: 2PC相对于其他分布式事务协议来说,相对简单,容易理解和实现。这使得它在某些情况下是一种合适的选择。

缺点

  1. 性能开销: 2PC协议引入了额外的性能开销。在第一阶段,需要协调者向参与者发送请求,等待参与者的响应,这会增加事务的延迟。此外,2PC需要协调者维护事务的状态信息,这也会占用资源
  2. 阻塞: 2PC的第一阶段可能会导致系统的阻塞,因为协调者需要等待所有参与者的响应。如果某个参与者长时间无响应,那么其他事务肯呢个也会受到影响。
  3. 单点故障: 2PC协议中的协调者是一个单点,如果协调者发生故障,整个分布式事务肯呢个无法进行。虽然可以通过备份协调者来提高可用性,但这也增加了复杂性。
  4. 不适用长事务: 2PC协议不适用于长时间运行的事务,因为它会导致参与者长时间锁定资源,从而影响其他事务的执行。
  5. 锁定问题: 在2PC中,参与者在第一阶段同意事务后,可能需要锁定资源,这可能导致资源的浪费和系统性能问题。

总之

3PC

3PC相对于2PC做了一定的改进: 引入了参与者超时机制,并且增加了预提交阶段使得在2PC执行中部分参与者不可用导致的其他参与者进行了资源锁定,进而需要事务回滚的操作。但是整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致的问题。

3PC包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是CanCommit、PreCommit和DoCommit。

其实就是在2PC的两个阶段之前添加了一个询问阶段,这个准备阶段只是询问了各个参与者状态是否支持

预提交阶段就是和2PC的准备阶段一样,除了事务的提交该做的都做了。

只是减少了部分由于部分参与者当时发生故障而导致其他参与者都已经进行了事务资源锁定,还需要协调者发送回滚指令的场景。 并没有真正的解决2PC两阶段提交的问题。

image.png

因为多引入了一个阶段,也就多了一次网络交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。

参与者超时带来的影响

超时机制: 参与者在接收到准备指令之后,在等待提交命令超时,那么参与者就会提交事务。因为都到了这一阶段了大概率是提交的。但是如果是因为网络波动问题导致没有接收到回滚指令,那么就会出现数据不一致的情况。

TCC

2PC和3PC都是数据库层面的,而TCC是业务层面的分布式事务,比如业务场景中不只是将数据就到数据库还会有发送MQ和记录到Redis的场景。

TCC 指的是Try-Confirm-Cancel

  • Try 指的是预留,即资源的预留和锁定,注意是预留
  • Confirm 指的是确认操作,这一步其实就是真正的执行了。
  • Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。

其实从思想上看和2PC差不多,都是先试探性的执行,如果都可以那就进行真正的执行,如果不行就回滚。

比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。

我们来看一下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。

image.png

可以看到流程还是很简单的,难点在于业务上的定义,对于每一个操作都需要邓毅三个动作分别对应Try、Confirm和Cancel 也就是三份代码逻辑。

因此TCC对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。

还有一点需要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。

相较于2PC、3PC,TCC适用的范围更大,但是开发量也更大,毕竟都在业务上实现,不过这三个方法还真不好写,不过也因为是在业务上实现,所以TCC可以跨数据库、跨不同的业务系统来实现事务。

TCC优缺点

优点:

  1. 精细控制: TCC允许开发人员在每个事务阶段(尝试、确认、取消)中编写自定义逻辑,从而可以实现精细的事务控制。这使得它适用于需要特定一致性要求的场景。
  2. 高可用性: TCC不依赖于中心化的协调者,每个参与者可以独立执行各个阶段,这有助于提高系统的可用性。即使某个参与者发生故障,其他参与者仍然可以继续完成事务的其余部分。
  3. 幂等性: TCC鼓励实现事务操作的幂等性,因为在取消操作阶段可能需要多次尝试。这有助于降低操作的重复执行风险。
  4. 可扩展性: TCC对于微服务架构和分布式系统的可扩展性较好,因为每个参与者可以独立决定事务的最终状态,而不需要全局协调。

缺点:

  1. 复杂性: TCC的实现相对复杂,需要开发人员编写额外的逻辑来处理尝试、确认和取消阶段。这增加了代码复杂性和维护成本。
  2. 事务长时间运行风险: 如果事务在尝试阶段占用资源的时间过长,可能会导致资源被锁定,影响其他事务的执行。
  3. 一致性难以保证: 尽管TCC提供了一种方式来实现分布式事务的最终一致性,但强一致性要求难以满足。在某些情况下,如果确认或取消阶段失败,可能需要手动干预以保证一致性。
  4. 幂等性要求: 要求事务操作是幂等的,这意味着相同操作可以多次执行而不会产生不一致的结果。这可能需要额外的开发工作来确保幂等性。
  5. 无法用于所有场景: TCC适合需要灵活一致性控制的场景,但不一定适用于所有分布式事务需求。在某些情况下,可能需要考虑其他分布式事务解决方案。

总的来说,TCC是一种适用于特定需求和场景的分布式事务解决方案。它提供了更精细的控制和高可用性,但要求开发人员处理复杂性和考虑一致性的问题。选择是否采用TCC取决于系统需求、复杂性和可维护性的权衡。

本地消息表

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

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

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

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

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

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

本地消息表优缺点

优点

  1. 灵活性: 本地消息表允许开发人员在不同微服务之间进行分布式事务管理,而不依赖单个全局事务协调器,这提供了更大的灵活性。
  2. 降低耦合度: 使用本地消息表可以减少分布式系统中不同微服务之间的耦合度。各个微服务只需将消息写入消息队列,而无需关心其他微服务间的具体实现。
  3. 容错性: 本地消息表允许事务消息的可靠异步处理,即使一个微服务发生故障或延迟,消息仍然可以在稍后的时间被处理,从而提高了系统的容错性。
  4. 异步处理: 本地消息表允许事务消息的异步处理,这有助于提高系统的响应速度和吞吐量。不需要等待所有参与者即时响应,可以继续处理其他请求。

缺点

  1. 消息处理的复杂性: 实现本地消息表需要额外的代码和逻辑,包括消息的发送、接收、处理和状态管理。这增加了系统的复杂性和维护成本。
  2. 一致性保证: 本地消息表虽然提供了最终一致性的保证,但它不同于强一致性协议(如2PC),在某些情况下,可能无法满足某些特定的一致性要求。开发人员需要仔细考虑业务需求,确保满足一致性要求。
  3. 消息丢失风险: 使用消息队列的方式存在消息丢失的风险。如果消息在发送和接收之间丢失,或者在处理中出现问题,肯呢个会导致事务无法达到一致状态。
  4. 重复消息: 由于网络故障或者消息队列问题,消息肯定会重复传递给接收者。开发人员需要实现去重逻辑来处理重复消息。
  5. 维护成本: 本地消息表需要数据库表的维护,定期清理已处理的消息。这可能需要额外的维护工作。

消息事务

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

第一步先给Broker发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向Broker发送Commit或者RollBack命令。

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

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

如果是RollBack那么订阅方收不到这条消息,等于事务就没执行过。

可以看到通过RocketMQ还是比较容易实现的,RocketMQ提供了事务消息的功能,我们只需要定义好事务反查接口即可。

image.png

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

最大努力通知

其实我觉得本地消息表也可以算是最大努力,事务消息也可以算是最大努力。

就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候,可以记录下然后引入人工,或者直接舍弃,这其实算是最大努力了。

事务消息也是一样,当半消息被Commit之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算是最大努力。

所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽我最大的努力想达成事务的最终一致了。

适用于对时间不敏感的业务,例如短信通知。

总结

可以看出2PC和3PC是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。

而TCC是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。

本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。