蚂蚁金服架构演进-服务化拆分
从单系统到微服务转变,实际上是一个资源横向扩展的过程,资源的横向扩展是指当单台机器达到资源性能瓶颈,没法知足业务增加需求时,就须要横向扩展资源,造成集群。经过横向扩展资源,提高非热点数据的并发性能,这对于大致量的互联网产品来讲,是相当重要的。服务的拆分,也能够认为是资源的横向扩展,只不过方向不一样而已。
资源横向扩展可能沿着两个方向发展,包括业务拆分和数据分片:
1.业务拆分:根据功能对数据进行分组,并将不一样的微服务分布在多个不一样的数据库上,这实际上就是SOA架构下的服务化。业务拆分就是把业务逻辑从一个单系统拆分到多个微服务中。
2.数据分片:在微服务内部将数据拆分到多个数据库上,为横向扩展增长一个新的维度。数据分片就是把一个微服务下的单个DB拆分红多个DB,具有一个Sharding的功能。经过这样的拆解,至关于一种资源的横向扩展,从而使得整个结构能够承载更高的吞吐
横向扩展的两种方法能够同时进行运用:交易、支付与帐务三个不一样微服务能够存储在不一样的数据库中。另外,每一个微服务内根据其业务量能够再拆分到多个数据库中,各微服务能够相互独立地进行扩展。
Seata 关注的就是微服务架构下的数据一致性问题,是一整套的分布式事务解决方案。Seata 框架包含两种模式,一种是 AT 模式。AT 模式主要从数据分片的角度,关注多 DB 访问的数据一致性,固然也包括多服务下的多 DB 数据访问一致性问题。
另一个就是 TCC 模式,TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题,保证读资源访问的事务属性。
TCC模式
Seata 框架把每组 TCC 接口当作一个 Resource,称为 TCC Resource。这套 TCC 接口能够是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自动扫描识别到 TCC 接口的调用方和发布方。若是是 RPC 的话,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。
若是是调用方,Seata 框架会给调用方加上切面,与 AT 模式同样,在运行时,该切面会拦截全部对 TCC 接口的调用。每调用一次 TCC 接口,切面会先向 TC(Transaction Coordinator,事务协调器) 注册一个分支事务,而后才去执行原来的 RPC 调用。当请求链路调用完成后,TC 经过分支事务的资源 ID 回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 方法。
TCC接口怎么实现?
框架自己很简单,主要是扫描TCC接口,注册资源,拦接口调用,注册分支事务,最后回调二阶段接口。最核心的其实是TCC接口的实现逻辑。
TCC业务模式与并发控制
TCC设计原则
从 TCC 模型的框架能够发现,TCC 模型的核心在于 TCC 接口的设计。用户在接入 TCC 时,大部分工做都集中在如何实现 TCC 服务上。下面我会分享蚂蚁金服内多年的 TCC 应用实践以及在 TCC 设计和实现过程当中的注意事项。
设计一套 TCC 接口最重要的是什么?主要有两点,第一点,须要将操做分红两阶段完成。TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖 RM 对分布式事务的支持,而是经过对业务逻辑的分解来实现分布式事务。
TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不肯定性,即对业务逻辑初步操做的调用仅是一个临时性操做,调用它的主业务服务保留了后续的取消权。若是主业务服务认为全局事务应该回滚,它会要求取消以前的临时性操做,这就对应从业务服务的取消操做。而当主业务服务认为全局事务应该提交时,它会放弃以前临时性操做的取消权,这对应从业务服务的确认操做。每个初步操做,最终都会被确认或取消。所以,针对一个具体的业务服务,TCC 分布式事务模型须要业务系统提供三段业务逻辑:
-
初步操做 Try:完成全部业务检查,预留必须的业务资源。
-
确认操做 Confirm:真正执行的业务逻辑,不作任何业务检查,只使用 Try 阶段预留的业务资源。所以,只要 Try 操做成功,Confirm 必须能成功。另外,Confirm 操做需知足幂等性,保证一笔分布式事务能且只能成功一次。
-
取消操做 Cancel:释放 Try 阶段预留的业务资源。一样的,Cancel 操做也须要知足幂等性。
第二点,就是要根据自身的业务模型控制并发,这个对应 ACID 中的隔离性。
这就是一个最简单的扣钱和加钱的 TCC 资源的设计。在扣钱 TCC 资源里,Try 接口预留资源扣除余额,Confirm 接口空操做,Cancel 接口释放资源,增长余额。在加钱 TCC 资源里,Try 接口无需预留资源,空操做;Confirm 接口直接增长余额;Cancel 接口无需释放资源,空操做。
2.3 帐务系统模型并发控制
以前提到,设计一套 TCC 接口须要有两点,一点是须要拆分业务逻辑成两阶段完成。这个咱们已经介绍了。另一点是要根据自身的业务模型控制并发。
Seata 框架自己仅提供两阶段原子提交协议,保证分布式事务原子性。事务的隔离须要交给业务逻辑来实现。隔离的本质就是控制并发,防止并发事务操做相同资源而引发的结果错乱。
举个例子,好比金融行业里管理用户资金,当用户发起交易时,通常会先检查用户资金,若是资金充足,则扣除相应交易金额,增长卖家资金,完成交易。若是没有事务隔离,用户同时发起两笔交易,两笔交易的检查都认为资金充足,实际上却只够支付一笔交易,结果两笔交易都支付成功,致使资损。
能够发现,并发控制是业务逻辑执行正确的保证,可是像两阶段锁这样的并发访问控制技术要求一直持有数据库资源锁直到整个事务执行结束,特别是在分布式事务架构下,要求持有锁到分布式事务第二阶段执行结束,也就是说,分布式事务会加长资源锁的持有时间,致使并发性能进一步降低。
所以,TCC 模型的隔离性思想就是经过业务的改造,在第一阶段结束以后,从底层数据库资源层面的加锁过渡为上层业务层面的加锁,从而释放底层数据库锁资源,放宽分布式事务锁协议,将锁的粒度降到最低,以最大限度提升业务并发性能。
仍是以上面的例子举例,“帐户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除 30 元,出现并发”。在第一阶段 Try 操做中,须要先利用数据库资源层面的加锁,检查帐户可用余额,若是余额充足,则预留业务资源,扣除本次交易金额,一阶段结束后,虽然数据库层面资源锁被释放了,但这笔资金被业务隔离,不容许除本事务以外的其它并发事务动用。
并发的事务 T2 在事务 T1 一阶段接口结束释放了数据库层面的资源锁之后,就能够继续操做,跟事务 T1 同样,加锁,检查余额,扣除交易金额。
事务 T1 和 T2 分别扣除的那一部分资金,相互之间无干扰。这样在分布式事务的二阶段,不管 T1 是提交仍是回滚,都不会对 T2 产生影响,这样 T1 和 T2 能够在同一个帐户上并发执行。
你们能够感觉下,一阶段结束之后,实际上采用业务加锁的方式,隔离帐户资金,在第一阶段结束后直接释放底层资源锁,该用户和卖家的其余交易均可以马上并发执行,而不用等到整个分布式事务结束,能够得到更高的并发交易能力。
这里稍微有点抽象,下面咱们将会针对业务模型进行优化,你们能够更直观的感觉业务加锁的思想。
2.4 帐务系统模型优化
前面的模型你们确定会想,为啥一阶段就把钱扣除了?是的。以前只是为了简单说明 TCC 模型的设计思想。在实际中,为了更好的用户体验,在第一阶段,通常不会直接把帐户的余额扣除,而是冻结,这样给用户展现的时候,就能够很清晰的知道,哪些是可用余额,哪些是冻结金额。
那业务模型变成什么样了呢?如图所示,须要在业务模型中增长冻结金额字段,用来表示帐户有多少金额处以冻结状态。
既然业务模型发生了变化,那扣钱和加钱的 TCC 接口也应该相应的调整。仍是之前面的例子来讲明。
在扣钱的 TCC 资源里。Try 接口再也不是直接扣除帐户的可用余额,而是真正的预留资源,冻结部分可用余额,即减小可用余额,增长冻结金额。Confirm 接口也再也不是空操做,而是使用 Try 接口预留的业务资源,即将该部分冻结金额扣除;最后在 Cancel 接口里,就是释放预留资源,把 Try 接口的冻结金额扣除,增长帐户可用余额。加钱的 TCC 资源因为不涉及冻结金额的使用,因此无需更改。
那并发控制又变成什么样了呢?跟前面大部分相似,在事务 T1 的第一阶段 Try 操做中,先锁定帐户,检查帐户可用余额,若是余额充足,则预留业务资源,减小可用余额,增长冻结金额。并发的事务 T2 相似,加锁,检查余额,减小可用余额金额,增长冻结金额。
这里能够发现,事务 T1 和 T2 在一阶段执行完成后,都释放了数据库层面的资源锁,可是在各自二阶段的时候,相互之间并没有干扰,各自使用本事务内第一阶段 Try 接口内冻结金额便可。这里你们就能够直观感觉到,在每一个事务的第一阶段,先经过数据库层面的资源锁,预留业务资源,即冻结金额。虽然在一阶段结束之后,数据库层面的资源锁被释放了,可是第二阶段的执行并不会被干扰,这是由于数据库层面资源锁释放之后经过业务隔离的方式为这部分资源加锁,不容许除本事务以外的其它并发事务动用,从而保证该事务的第二阶段可以正确顺利的执行。
经过这两个例子,为你们讲解了怎么去设计一套完备的 TCC 接口。最主要的有两点,一点是将业务逻辑拆分红两个阶段完成,即 Try、Confirm、Cancel 接口。其中 Try 接口检查资源、预留资源、Confirm 使用资源、Cancel 接口释放预留资源。另一点就是并发控制,采用数据库锁与业务加锁的方式结合。因为业务加锁的特性不影响性能,所以,尽量下降数据库锁粒度,过渡为业务加锁,从而提升业务并发能力。
3 TCC 异常控制
在有了一套完备的 TCC 接口以后,是否是就真的高枕无忧了呢?答案是否认的。在微服务架构下,颇有可能出现网络超时、重发,机器宕机等一系列的异常 Case。一旦遇到这些 Case,就会致使咱们的分布式事务执行过程出现异常。根据蚂蚁金服内部多年的使用来看,最多见的主要是这三种异常,分别是空回滚、幂等、悬挂。
所以,TCC 接口里还须要解决这三类异常。实际上,这三类问题能够在 Seata 框架里完成,只不过咱们如今的 Seata 框架还不具有,以后咱们会把这些异常 Case 的处理移植到 Seata 框架里,业务就无需关注这些异常状况,专一于业务逻辑便可。
虽然业务以后无需关心,可是了解一下其内部实现机制,也能更好的排查问题。下面我将为你们一一讲解这三类异常出现的缘由以及对应的解决方案。
3.1 空回滚
首先是空回滚。什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的状况下,调用了二阶段的 Cancel 方法,Cancel 方法须要识别出这是一个空回滚,而后直接返回成功。
什么样的情形会形成空回滚呢?能够看图中的第 2 步,前面讲过,注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,先向 TC 注册一个分支事务,而后才去执行 RPC 调用逻辑。若是 RPC 调用逻辑有问题,好比调用方机器宕机、网络异常,都会形成 RPC 调用失败,即未执行 Try 方法。可是分布式事务已经开启了,须要推动到终态,所以,TC 会回调参与者二阶段 Cancel 接口,从而造成空回滚。
那会不会有空提交呢?理论上来讲不会的,若是调用方宕机,那分布式事务默认是回滚的。若是是网络异常,那 RPC 调用失败,发起方应该通知 TC 回滚分布式事务,这里能够看出为何是理论上的,就是说发起方能够在 RPC 调用失败的状况下依然通知 TC 提交,这时就会发生空提交,这种状况要么是编码问题,要么开发同窗明确知道须要这样作。
那怎么解决空回滚呢?前面提到,Cancel 要识别出空回滚,直接返回成功。那关键就是要识别出这个空回滚。思路很简单就是须要知道一阶段是否执行,若是执行了,那就是正常回滚;若是没执行,那就是空回滚。所以,须要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,若是该记录存在,则正常回滚;若是该记录不存在,则是空回滚。
3.2 幂等
接下来是幂等。幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,所以,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。若是幂等控制没有作好,颇有可能致使资损等严重问题。
什么样的情形会形成重复提交或回滚?从图中能够看到,提交或回滚是一次 TC 到参与者的网络调用。所以,网络故障、参与者宕机等都有可能形成参与者 TCC 资源实际执行了二阶段防范,可是 TC 没有收到返回结果的状况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。
怎么解决重复执行的幂等问题呢?一个简单的思路就是记录每一个分支事务的执行状态。在执行前状态,若是已执行,那就再也不执行;不然,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那咱们彻底能够在这张事务控制表上加一个状态字段,用来记录每一个分支事务的执行状态。
如图所示,该状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。二阶段 Confirm 和 Cancel 方法执行后修改成已提交或已回滚状态。当重复调用二阶段接口时,先获取该事务控制表对应记录,检查状态,若是已执行,则直接返回成功;不然正常执行。
3.3 悬挂
最后是防悬挂。按照惯例,我们来先讲讲什么是悬挂。悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。由于容许空回滚的缘由,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来讲,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。可是这以后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种状况时,该分布式事务第一阶段预留的业务资源就再也没有人可以处理了,对于这种状况,咱们就称为悬挂,即业务资源预留后无法继续处理。
什么样的状况会形成悬挂呢?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,若是此时 RPC 调用的网络发生拥堵,一般 RPC 调用是有超时时间的,RPC 超时之后,发起方就会通知 TC 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者,真正执行,从而形成悬挂。
怎么实现才能作到防悬挂呢?根据悬挂出现的条件先来分析下,悬挂是指二阶段 Cancel 执行完后,一阶段才执行。也就是说,为了不悬挂,若是二阶段执行完成,那一阶段就不能再继续执行。所以,当一阶段执行时,须要先检查二阶段是否已经执行完成,若是已经执行,则一阶段再也不执行;不然能够正常执行。那怎么检查二阶段是否已经执行呢?你们是否想到了刚才解决空回滚和幂等时用到的事务控制表,能够在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,若是记录存在,就认为二阶段已经执行;不然二阶段没执行。
3.3 异常控制实现
在分析完空回滚、幂等、悬挂等异常 Case 的成因以及解决方案之后,下面咱们就综合起来考虑,一个 TCC 接口如何完整的解决这三个问题。
首先是 Try 方法。结合前面讲到空回滚和悬挂异常,Try 方法主要须要考虑两个问题,一个是 Try 方法须要可以告诉二阶段接口,已经预留业务资源成功。第二个是须要检查第二阶段是否已经执行完成,若是已完成,则再也不执行。所以,Try 方法的逻辑能够如图所示:
先插入事务控制表记录,若是插入成功,说明第二阶段尚未执行,能够继续执行第一阶段。若是插入失败,则说明第二阶段已经执行或正在执行,则抛出异常,终止便可。
接下来是 Confirm 方法。由于 Confirm 方法不容许空回滚,也就是说,Confirm 方法必定要在 Try 方法以后执行。所以,Confirm 方法只须要关注重复提交的问题。能够先锁定事务记录,若是事务记录为空,则说明是一个空提交,不容许,终止执行。若是事务记录不为空,则继续检查状态是否为初始化,若是是,则说明一阶段正确执行,那二阶段正常执行便可。若是状态是已提交,则认为是重复提交,直接返回成功便可;若是状态是已回滚,也是一个异常,一个已回滚的事务,不能从新提交,须要可以拦截到这种异常状况,并报警。
最后是 Cancel 方法。由于 Cancel 方法容许空回滚,而且要在先执行的状况下,让 Try 方法感知到 Cancel 已经执行,因此和 Confirm 方法略有不一样。首先依然是锁定事务记录。若是事务记录为空,则认为 Try 方法还没执行,便是空回滚。空回滚的状况下,应该先插入一条事务记录,确保后续的 Try 方法不会再执行。若是插入成功,则说明 Try 方法尚未执行,空回滚继续执行。若是插入失败,则认为 Try 方法正再执行,等待 TC 的重试便可。若是一开始读取事务记录不为空,则说明 Try 方法已经执行完毕,再检查状态是否为初始化,若是是,则尚未执行过其余二阶段方法,正常执行 Cancel 逻辑。若是状态为已回滚,则说明这是重复调用,容许幂等,直接返回成功便可。若是状态为已提交,则一样是一个异常,一个已提交的事务,不能再次回滚。
经过这一部分的讲解,你们应该对 TCC 模型下最多见的三类异常 Case,空回滚、幂等、悬挂的成因有所了解,也从实际例子中知道了怎么解决这三类异常,在解决了这三类异常的状况下,咱们的 TCC 接口设计就是比较完备的了。后续咱们将会把这些解决方案移植到 Seata 框架中,由 Seata 框架来完成异常的处理,开发 TCC 接口的同窗就再也不须要关心了。
4 TCC 性能优化
虽然 TCC 模型已经完备,可是随着业务的增加,对于 TCC 模型的挑战也愈来愈大,可能还须要一些特殊的优化,才能知足业务需求。下面咱们将会给你们讲讲,蚂蚁金服内部在 TCC 模型上都作了哪些优化。
4.1 同库模式
第一个优化方案是改成同库模式。同库模式简单来讲,就是分支事务记录与业务数据在相同的库中。什么意思呢?以前提到,在注册分支事务记录的时候,框架的调用方切面会先向 TC 注册一个分支事务记录,注册成功后,才会继续往下执行 RPC 调用。TC 在收到分支事务记录注册请求后,会往本身的数据库里插入一条分支事务记录,从而保证事务数据的持久化存储。那同库模式就是调用方切面再也不向 TC 注册了,而是直接往业务的数据库里插入一条事务记录。
在讲解同库模式的性能优化点以前,先给你们简单讲讲同库模式的恢复逻辑。一个分布式事务的提交或回滚仍是由发起方通知 TC,可是因为分支事务记录保存在业务数据库,而不是 TC 端。所以,TC 不知道有哪些分支事务记录,在收到提交或回滚的通知后,仅仅是记录一下该分布式事务的状态。那分支事务记录怎么真正执行第二阶段呢?须要在各个参与者内部启动一个异步任务,按期捞取业务数据库中未结束的分支事务记录,而后向 TC 检查整个分布式事务的状态,即图中的 StateCheckRequest 请求。TC 在收到这个请求后,会根据以前保存的分布式事务的状态,告诉参与者是提交仍是回滚,从而完成分支事务记录。
那这样作有什么好处呢?左边是采用同库模式前的调用关系图,在每次调用一个参与者的时候,都是先向 TC 注册一个分布式事务记录,TC 再持久化存储在本身的数据库中,也就是说,一个分支事务记录的注册,包含一次 RPC 和一次持久化存储。
右边是优化后的调用关系图。从图中能够看出,每次调用一个参与者的时候,都是直接保存在业务的数据库中,从而减小与 TC 之间的 RPC 调用。优化后,有多少个参与者,就节约多少次 RPC 调用。
这就是同库模式的性能方案。把分支事务记录保存在业务数据库中,从而减小与 TC 的 RPC 调用。
4.2 异步化
另一个性能优化方式就是异步化,什么是异步化。TCC 模型的一个做用就是把两阶段拆分红了两个独立的阶段,经过资源业务锁定的方式进行关联。资源业务锁定方式的好处在于,既不会阻塞其余事务在第一阶段对于相同资源的继续使用,也不会影响本事务第二阶段的正确执行。从理论上来讲,只要业务容许,事务的第二阶段何时执行均可以,反正资源已经业务锁定,不会有其余事务动用该事务锁定的资源。
假设只有一个中间帐户的状况下,每次调用支付服务的 Commit 接口,都会锁定中间帐户,中间帐户存在热点性能问题。
可是,在担保交易场景中,七天之后才须要将资金从中间帐户划拨给商户,中间帐户并不须要对外展现。所以,在执行完支付服务的第一阶段后,就能够认为本次交易的支付环节已经完成,并向用户和商户返回支付成功的结果,并不须要立刻执行支付服务二阶段的 Commit 接口,等到低锋期时,再慢慢消化,异步地执行。
5 总结
今天进行了 Seata TCC 模式的深度解析。主要介绍 Seata TCC 模式的原理,从 TCC 业务模型与并发控制的角度告诉你们怎么设计一个 TCC 的接口以及怎么处理空回滚、幂等、悬挂等异常,最后对蚂蚁金服内部对 TCC 的性能优化点简单介绍,使得 TCC 模式可以知足更高的业务需求。
业务各有不一样,有些业务能容忍短时间不一致,有些业务的操做能够幂等,不管什么样的分布式事务解决方案都有其优缺点,没有一个银弹可以适配全部。所以,业务须要什么样的解决方案,还须要结合自身的业务需求、业务特色、技术架构以及各解决方案的特性,综合分析,才能找到最适合的方案。