这几天在学习过程中说到了分布式事务的问题,在网上查了许多资料,参考了小米信息技术部海外商城组,李文华文章,写下了这篇博客,作为学习笔记。!
随着微服务的盛行,越来越多的企业将应用完成了从单体架构转向了分布式架构的转变。而谈到微服务、分布式架构,我们不得不谈分布式事务,而在说分布式事务之前,我们有必要先了解一下什么是事务。

事务

事务是在应用程序中一系列严密的操作,所有的操作要不全部完成,要不全都不做。也就是说事务必须具备原子性。事务必须遵循四个特性:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability),即ACID属性。

分布式事务

分布式事务是指事务的参与者、支持事务的服务器、事务管理器以及资源服务器位于不同的分布式系统的不同节点上。例如在大型的电商系统中,下单接口通常会减库存、生成订单、完成支付、而订单服务和库存服务以及支付服务都是不同的服务,下单成功与否,不仅仅取决于下单接口,还取决于库存和支付服务是否成功,如果其中有一个失败了,那么下单就不成功。这就是分布式事务。也就是说分布式事务的本质就是保证不同的数据库在数据上保持一致。

一致性的等级

强一致性

任何一次读操作,都能读到系统的最近一次写的操作,即系统中任意时刻、任意节点的同一份数据都是一样的。

弱一致性

数据更新后,如果能容忍部分数据访问不到或者全部数据访问不到,则是弱一致性

最终一致性

不保证任意时刻和任意节点的同一份数据一致,但在时间的作用下,所有节点上的同一份数据都是向相同的方向进行的。即在一段时间后,所有节点上的同一份数据最终会保持一致。

CAP原则

C(Consistenc):一致性

在分布式系统中的同一份数据,在同一时刻是否具有相同的值。CAP中的一致性指的是强一致性。

A(Aiailability):可用性

在系统中的一部分节点故障后,整个系统仍然可用

P(Partition tolerance):分区容错性

系统中任意信息的丢失或失败不会影响系统的继续运作。

CAP理论就是说在上面三点中要么AP,要么CP,要么AC,不可能同时满足。

CAP的取舍

在分布式系统中,由于网络硬件肯定会出现延迟丢包等问题,所以分区容错性是我们必须需要实现的。所以我们只能在AC之间做出取舍。

  • 满足CP舍弃A,也就是满足一致性和容错性,舍弃可用性。如果你的系统允许有段时间的访问失效问题,好比并发买票,后台网络出现故障,那么你的系统就崩溃了。
  • 满足AP舍弃C,也就是满足可用性和容错性,舍弃一致性。这也就是意味着你的系统在并发访问的时候可能会出现数据不一致的情况。就好比是你买火车票,本来你看到的是还有一张票,其实在这个时刻已经被买走了,你填好了信息准备买的时候发现系统提示你没票了。这就是牺牲了一致性。

BASE理论

BASE理论指的是基本可用Basically Available,软状态Soft State,最终一致Eventual Consistency三个短语的缩写。
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。 接下来我们着重对BASE中的三要素进行详细讲解。基本可用:指分布式系统在出现不可预知故障的时候,允许损失部分可用性。

基本可用,注意,这不等于系统绝对不可用

  • 响应速度上的损失:后台出现故障,接口响应时间增加。
  • 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

软状态

和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

最终一致性

强调的是系统中的数据在一段时间后最终能够达到一个一致的状态,可以接受一段时间内不同,但最终的数据一定是相同的。

分布式事务使用场景

转账

转账是事务最经典的场景,假设用户 A 使用银行 app 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额。此时就会出现 2 种异常情况:1. 用户 A 的账户扣款成功,用户 B 账户余额增加失败 2. 用户 A 账户扣款失败,用户 B 账户余额增加成功。对于银行系统来说,以上 2 种情况都是不允许发生,此时就需要分布式事务来保证转账操作的成功。

下单扣库存

在电商系统中,下单是用户最常见的操作,而下单必定会涉及生成订单编号以及扣除库存等操作,对于微服务架构,订单与库存一般都是单独的服务,此时就需要分布式事务来保证下单操作的成功。

同步超时

还是以电商为例,在微服务体系架构下,我们的支付与订单都是作为单独的服务存在,而订单的支付状态依赖于支付系统的通知。假设一个场景,我们的支付系统收到第三方支付系统(微信、支付宝)的通知,告知某个订单支付成功,此时我们支付系统需要调用订单系统更新订单的支付状态。
流程图如下
image.png
从图中可以看出有两次调用,第三方支付调用支付服务,以及支付服务调用订单服务,这两步调用都可能出现调用失败(超时或异常)的情况,此处如果没有分布式事务的保证,就会出现用户订单实际支付情况与最终用户看到的订单支付情况不一致的情况。

分布式事务解决方案

两阶段提交(2CP)/XA

两阶段提交,顾名思义就是分两个阶段提交。此时,在系统中存在一个协调两个服务的事务管理器,事务管理器在第一阶段时发出请求询问各个服务是否准备就绪,如果都准备就绪,则进行第二阶段,如果有一个失败,就回滚事务。
image.png
image.png
大致流程:
第一阶段(prepare):事务管理器向该事务参与者的本地资源管理器发起请求,询问是否是 ready 状态,所有资源管理器都将本事务能否成功的信息反馈发给协调者;
第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。
存在的问题:

  1. 同步阻塞:当参与事务者存在占用公共资源的情况,其中一个占用了资源,其他事务参与者就只能阻塞等待资源释放,处于阻塞状态。
  2. 单点故障:一旦事务管理器故障,整个系统不可用。
  3. 数据不一致:在第二阶段发送commit时,发送部分,事务管理器出现故障,此时只有部分资源管理器收到commit消息,也就是说只有部分事务参与者提交了事务,导致系统数据不一致。
  4. 不确定性:当事务管理器发送commit后,并且只有一个或部分参与者收到了消息时事务管理器出现故障,于此同时事务参与者也出现了故障,那么重新启动的事务管理器无法确定该事务参与者的commit到底有没有执行成功。

TCC

关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:

  • 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

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 是消息消费者,其大致流程如下:
image.png

  1. 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中
  2. 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试
  3. 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作

本地消息表实现的条件:

  1. 消费者与生产者的接口都要支持幂等
  2. 生产者需要额外的创建消息表
  3. 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作

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

可靠消息最终一致性

其流程如下:
image.png

  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 次,最后还是不行就放弃。

分布式实战

两阶段提交(2CP)/XA

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

TCC

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

可靠消息最终一致性

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

本地消息表

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

最大努力通知

最大努力通知最常见的场景就是支付回调,支付服务收到第三方服务支付成功通知后,先更新自己库中订单支付状态,然后同步通知订单服务支付成功。如果此次同步通知失败,会通过异步脚步不断重试地调用订单服务的接口。
image.png
小米海外商城目前除了支付回调外,最常用的场景是订单数据同步。例如系统 A、B 进行数据同步,当系统 A 发生订单数据变更,先将数据变更消息写入小米 notify 系统(作用等同 mq),然后 notify 系统异步处理该消息来调用系统 B 提供的接口并进行重试到最大次数。

image.png