基础理论

事务

ACID特性,要么全部成功要么一个都不做没有中间状态。原子性、一致性、隔离性、持久性。

分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点上。例如大型电商系统中,下单接口通常会扣减库存、减去优惠、生成订单id,而订单服务与库存、优惠、订单id都是不同的服务,下单接口的成功与否,不仅仅取决于本地db操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是保证不同数据库的数据的一致性。

一致性(强,弱,最终)

强一致性

系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致
强一致性破坏可用性

弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到
redis集群默认是弱一致性

最终一致性

不保证任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。节点间的数据最终达到一致状态。

CAP原则

CAP原则

BASE理论

BASE理论

柔性事务

不同于ACID的刚性事务,在分布式场景下基于BASE理论,就出现了柔性事务的概念。想要通过柔性事务来达到最终的一致性,就需要依赖于一些特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样;但是都不满足的话,是不可能做柔性事务的。

幂等操作

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,页不用担心重复执行会对系统造成改变。例如,支付流程中第三方支付系统告知系统中某个订单支付成功,接收该支付回调接口在网络正常的情况下无论操作多少次都应该返回成功。

分布式事务使用场景

转账

转账是最经典的分布式事务场景,a跨行转账给b。a账户扣款,b账户余额增加
可能有两种异常情况:1,a扣款成功,b账户余额增加失败2、a扣款失败,b账户余额增加成功,对于银行以上两种都不允许发生,此时就需要分布式事务来保证转账操作的成功

充值提现

本地系统充值,创建本地消息表,充值状态为发起充值。调用第三方支付充值接口,第三方支付会同步返回,通常这个是指第三方支付系统收到这个请求并写入消费队列,本地消息表中充值状态为充值中。第三方支付消费者消费成功,会调用本地回调接口,即向本地服务发送http请求消息告知充值最终状态,本地服务根据次最终状态修改本地消息表中的状态为充值成功或者充值失败。

关注流水状态 充值算流水,提现算流水,投资算流水,到期返还本金和利息算流水。总之,用户账户金额的变化,都算流水。

下单扣库存

订单系统本地做一些简单的库存判断创建一个订单记录,向库存服务发起扣减库存请求。库存服务收到请求,同步返回接受成功状态,此时库存服务未处理真实业务,仅向消费队列中写入一条消息,订单系统本地消息表订单状态改为扣减库存中。库存服务消费成功,会调用订单系统库存回调接口,订单系统根据请求修改此订单状态。

关注订单状态

同步超时

以电商系统为例,在微服务架构下,我们的支付与订单都是作为单独的系统存在。订单的状态依赖支付系统的通知。假设一个场景:我们的支付系统收到来自第三方的支付通知,告知某个订单支付成功,接收通知接口需要同步调用订单服务变更状态接口,更新订单状态为成功。流程图如下,从途中可以看出,有两次点用,第三方支付调用支付服务,以及支付服务调用订单服务,着两部调用都可能出现调用超时的情况,此处没有分布式事务的保证,就会出现用户订单实际支付情况与最终用户看到的支付情况不一致的情况。

分布式事务解决方案

两阶段提交/XA

两阶段提交,顾名思义就是要分两步提交。存在一个负责协调各个本地资源管理器的事务管理器,本地资源管理器一般是由数据库实现,事务管理器在第一阶段的时候询问各个资源管理器是否都就绪?如果收到每个资源的回复都是 yes,则在第二阶段提交事务,如果其中任意一个资源的回复是 no, 则回滚事务。

第一阶段(prepare):事务管理器向所有本地资源管理器发起请求,询问是否是 ready 状态,所有参与者都将本事务能否成功的信息反馈发给协调者;

第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。

存在的问题:

同步阻塞:当参与事务者存在占用公共资源的情况,其中一个占用了资源,其他事务参与者就只能阻塞等待资源释放,处于阻塞状态。

单点故障:一旦事务管理器出现故障,整个系统不可用

数据不一致:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。

不确定性:当协事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

三阶段提交TCC

Try-Confirm-Cancel

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

  1. 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。

  2. 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。

  3. 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

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

在 Try 阶段,是对业务系统进行检查及资源预览,比如订单和存储操作,需要检查库存剩余数量是否够用,并进行预留,预留操作的话就是新建一个可用库存数量字段,Try 阶段操作是对这个可用库存数量进行操作。
基于 TCC 实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口,所以代码实现复杂度相对较高。

本地消息表

本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。该方案中会有消息生产者与消费者两个角色,假设系统 A 是消息生产者,系统 B 是消息消费者,其大致流程如下:

  1. 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中

  2. 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试

  3. 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作

本地消息表实现条件:

  1. 消费者与生产者的接口都要支持幂等

  2. 生产者需要额外的创建消息表

  3. 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作

容错机制:

  1. 步骤 1 失败时,事务直接回滚

  2. 步骤 2、3 写 mq 与消费 mq 失败会进行重试

  3. 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

可靠消息最终一致性

  1. A 系统先向 mq 发送一条 prepare 消息,如果 prepare 消息发送失败,则直接取消操作

  2. 如果消息发送成功,则执行本地事务

  3. 如果本地事务执行成功,则想 mq 发送一条 confirm 消息,如果发送失败,则发送回滚消息

  4. B 系统定期消费 mq 中的 confirm 消息,执行本地事务,并发送 ack 消息。如果 B 系统中的本地事务失败,会一直不断重试,如果是业务失败,会向 A 系统发起回滚请求

  5. mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况,如果该 prepare 消息本地事务处理成功,则重新发送 confirm 消息,否则直接回滚该消息

该方案与本地消息最大的不同是去掉了本地消息表,其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与容错方案基本一致。目前市面上实现该方案的只有阿里的 RocketMq。

尽最大努力通知

最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。

这个方案的大致意思就是:

  1. 系统 A 本地事务执行完之后,发送个消息到 MQ;

  2. 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;

  3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。

分布式事务实战

两阶段提交/XA

目前支付宝使用两阶段提交思想实现了分布式事务服务 (Distributed Transaction Service, DTS) ,它是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。具体可参考支付宝官方文档:

分布式事务服务简介

TCC

TCC 需要事务接口提供 try, confirm, cancel 三个接口,提高了编程的复杂性。依赖于业务方来配合提供这样的接口,推行难度大,所以一般不推荐使用这种方式。

可靠消息最终一致性

目前市面上支持该方案的 mq 只有阿里的 rocketmq, 该方案应用场景也比较多,比如用户注册成功后发送邮件、电商系统给用户发送优惠券等需要保证最终一致性的场景

本地消息表

跨行转账可通过该方案实现。
用户 A 向用户 B 发起转账,首先系统会扣掉用户 A 账户中的金额,将该转账消息写入消息表中,如果事务执行失败则转账失败,如果转账成功,系统中会有定时轮询消息表,往 mq 中写入转账消息,失败重试。mq 消息会被实时消费并往用户 B 中账户增加转账金额,执行失败会不断重试。

商城用户订单数据状态变更,会将变更状态记录消息表中,脚本将订单状态消息写入 mq,最终消费 mq 给用户发送邮件、短信、push 等。

最大努力通知

最大努力通知最常见的场景就是支付回调,支付服务收到第三方服务支付成功通知后,先更新自己库中订单支付状态,然后同步通知订单服务支付成功。如果此次同步通知失败,会通过异步脚步不断重试地调用订单服务的接口。

商城目前除了支付回调外,最常用的场景是订单数据同步。例如系统 A、B 进行数据同步,当系统 A 发生订单数据变更,先将数据变更消息写入 notify 系统(作用等同 mq),然后 notify 系统异步处理该消息来调用系统 B 提供的接口并进行重试到最大次数。

总结

本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解,在文章的后半部分主要给出了各种方案的常用场景。分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要根据自身业务特点自行选择,每种方案在实际执行过程中需要考虑的点都非常多,复杂度较大,所以在非必要的情况下,分布式事务能不用就尽量不用。

Scroll to Top