分布式事务——面向新金融的模型化分布式金融核心探秘之四

Posted 尚芸Fintech

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式事务——面向新金融的模型化分布式金融核心探秘之四相关的知识,希望对你有一定的参考价值。

天真的人们能够爱——这就是他们的秘密.


 

——赫尔曼•黑塞

       在中,我们阐述了在金融核心系统数据拆分上的实践,很显然,随之而来的就是分布式事务,这是目前在分布式理论和实践中的一大难题,也是金融机构对于分布式架构是否适合核心系统的最大担忧。不能解决这一问题,就意味着金融核心要求的数据一致性得不到保证。各种技术社区就这一问题也有大量探讨,也给出了各种不同方案,如XA分布式事务、可靠消息模式、补偿模式等等,但这些方案或者因为锁粒度较大,性能无法满足互联网环境要求,或者因为过程中部分业务已提交,可能导致无法回滚等等各种问题都无法满足互联网环境下的金融核心要求。阿里巴巴和蚂蚁金服在自身实践中提出了一种新的分布式事务处理模式,TCC。本质上TCC也是一个两阶段协议,但相对于XA,其是在应用层进行控制的两阶段协议,所以称之为服务化两阶段提交分布式事务。关于TCC细节,我们在这里就不赘述了,具体可以参见https://www.cloud.alipay.com/docs/2/46887


       相对其他各种分布式事务处理机制,它有以下特性:

      1. 较好的性能,不像XA那样过长时间地持有资源。由于在应用层面使用资源预留或保护模式,所有更改在本地事务完成后,即进行锁释放。

      2. 较简单的分布式模型,分布式协调者面对的是统一的技术构型,统一实现了prepare/confirm/cancel接口,分布式发起者和协调者基于这个模型构建,简化了构建的复杂度。

      3. 较少的软件依赖,这个模型实现为应用级别的功能实现而不依赖于特定的数据库或其他交易管理器。

      4. 对分布式数据的分库策略透明,由于使用应用级的事务接口,如上一篇文章的分库方式中介绍的,应用通过分库中间件进行分库策略化处理,每一个应用实例都是对等的,对于分布式事务发起者和协调者,只需要调用该应用的任意实例就能访问至数据分片

      

       TCC被称为服务化两阶段分布式事务,所谓服务化,就是重点要在服务,也就是应用层面作良好设计,所以这里我们并不想过多介绍TCC技术框架本身,而是更多关注应用,如何进行业务设计方能正确且方便的应用该框架,我们想重点讨论下面几个问题


1一致性和锁粒度

 

        事务一致性,其实并不是一个很容易阐释清楚的问题,我们倾向于这样一个定义:事务开始和结束之间的中间状态不会被其他事务看到。分布式事务中最核心的问题依然是一致性,XA协议使用了大粒度的锁来实现这样的一致性,因此导致性能无法满足互联网环境下的要求。

        那在TCC模式下是如何兼顾性能和一致性的呢,这个还需要结合具体业务来考虑。设计分布式事务最为关心的当然是写交易,或者进一步说是Update交易,我们知道金融核心业务中最大量的Update交易通常都是账户余额的变动,这可以说是金融核心系统中最热点的资源,对这一资源操作的性能常常决定着整个系统的性能,我们不能像XA协议一样对所有被更新余额锁定,直到整个分布式事务完成才释放锁,但又要保证被提前释放锁的余额可以实现一致性。在设计中尚芸解决方案采用了资源保护模式,也就是新增加字段表征这种保护。举一个例子,两个不同数据库中的A客户和B客户,各有1000元,这时发生交易T1,A向B转账100元。新增加一个冻结金额(freezing amount)来表明这种预留。在Prepare阶段,A客户freezing amount=0,balance amount=900,B客户freezing amount=100,balance amount=1100。在confirm或者recovery阶段,将对这个freezing amount进行清理。在A,B各自完成prepare之后,就已经释放了锁,这时候(也就是T1尚未confirm或者recovery的时候),如果又发生了另一个C转账到B 100元的交易T2,T2无需等待,在T2Prepare时,B客户freezing amount=200,balance amount=1200。假设此时又有一个交易请求从B向D转账1200元T3,因为T1和T2都还未完成,这时候计算B的available amount= balance amount - freezing amount=1000,所以交易请求T3失败。可以这样说,在转账这个分布式事务中,余额字段的中间状态通过一个freezing amount字段对其他事务进行了屏蔽。所以,如果传统的金融核心应用需要修改为分布式应用,首先就要这样的数据库和应用逻辑的修改。常见的传统金融核心应用没有进行过很好的数据和应用建模,这样的修改会影响到很多数据库表结构以及大多数交易的业务逻辑,修改起来难度相当大。而尚芸的模型化分布式金融核心因为基于抽象金融模型,所有业务都天然的就公用Balance这个统一的实体,对Balance一点的数据库表改造和业务逻辑改造就可以满足所有业务,甚至我们的金融核心都不需要进行数据库表改造,而只需要增加一种amount type,业务逻辑层开发人员通过调用Balance基础组件API即可实现对freezing amount, available amount等的设置和读取。


        除了账户余额这种字段,金融核心中还有大量非数值型字段通常这些字段都不算热点资源,对这些资源的一致性保证可以通过添加是否block的字段来保证其中间状态对其他事务屏蔽。这对传统金融核心系统来说也就意味着所有数据库表都需要增加一个block的字段,所有的业务逻辑都得首先查询所涉及到的所有表中的block字段。可以想见,这样的改动相对于账户余额这种处理方式的改动更加巨大。有没有更好的方法呢?大家如果记得的这段话“尚芸解决方案把上述物理数据模型中的上百个实体、几千个属性又在基础组件层重新聚合成产品、合约、结算、核算、参与人,资产等聚合,并分别分配了我们称为聚合根的实体,所有对聚合内的实体的操作都必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持对它的引用的唯一元素。”,应该对这个问题就有答案了。是的,我们只在聚合根的实体上进行block状态的控制,这样通过在聚合根实体上添加block字段,就可以达到对所有资源的中间状态控制。


2原子性


        我们知道关系型数据库实现原子性的关键手段是Redo/Undo log,分布式事务为了达到一致性,也需要实现事务的类原子性。但TCC本身的分布式事务技术框架只有主事务记录 (Activity)和分支事务记录 (Action)记录交易记录、主事务记录、分支事务记录等的跟踪关系,但对于过程中的具体业务数据修改历史,类似于Undo log,并没有给出持久化解决方案,需要使用者自行设计。对于这一设计,最朴素的想法就是对所有数据库表建立一张历史表,记录所有历史变更,在Recovery的时候使用历史值去恢复。但对于上面我们描述的余额字段这种资源保护设计,在并发操作的情况,简单的用历史值去恢复,可能会造成类似于第一类丢失更新的问题。尚芸解决方案对于这一问题的解法不是记录历史绝对值,而是记录每次的增减值,用增减的逆向操作来进行恢复。尚芸的数据模型在Transaction实体组中,天然设计了Entry实体,用来记录Arrangement的每次增建操作。另外,用一张与原表同构的历史表去恢复,也会造成一定的性能损失,所以使用KV的模式只对修改值进行记录是另一种可以选取的方式,因为尚芸的数据模型设计中,很多原表本身就采用了KV的模式,所以也比较容易以相同的方式去处理历史值。

       除了类Undo Log本身的记录,如何开发Recovery方法本身也并不是一件容易的事。因为对每一个微服务本身,都需要实现一个Recovery方法,传统核心银行少说也有上千个交易,也就是说要实现上千个Recovery方法。而且银行业务相对复杂,大多数交易都会涉及到合约信息的修改,余额信息的修改,交易的生成,流水的生成,账务的处理等等,正向交易本身的业务逻辑已经相当复杂,反向操作的逻辑更加难于理清,比如余额如何逆向操作,交易是否应该删除,流水是否应该删除,账务又应该如何调整等。如果需要每一个业务开发人员去书写每一个Recovery方法,势必会造成大量错误。所以我们认为Recovery方法不应该是一个业务的逻辑,而应该变成单纯的数据操作,并且是统一的。再次回到我们的金融模型,所有的交易进行的操作都被我们封装到了Balance和Transaction等基础组件,业务开发人员只需要调用Transaction.recovery()以及Balance.freezing ()等方法,就可以完成全部的Recovery。


3隔离性


       和关系型数据库一样,分布式事务其实也需要考虑事务隔离级别。在单体应用中,应用通常使用已提交读(Read Committed)的隔离级别,但在TCC模式下,因为表示事务状态的block字段是需要应用层面处理的,所以很难实现所谓的锁等待,这时候最好的解决方案就是通过界面展示告诉用户中间数据的存在,比如告诉用户当前正有交易修改该合约,或者当前冻结的金额多少。对于读交易,开发人员根据业务作出决定,是否需要这种中间状态展示给用户,但很显然,如在一致性中描述的,所有的写操作都需要首先去查询这个block状态,对block状态的记录不允许进行操作。


4幂等性


       TCC的参与者需要支持同一笔事务的重复confirm和重复recovery,这就是所谓幂等性要求,这个倒是并不困难,通过主事务记录 (Activity)和分支事务记录 (Action)中的主键进行判断即可完成,即每次调用的时候传入唯一键值,通过唯一键值判断业务是否被操作,如果已被操作,则不再重复操作。


5谁是发起者?参与者的粒度?

 

       TCC模式的分布式事务处理框架,有发起者和参与者的概念,并且要求每个分布式事务的参与者和发起者都要进行相关配置。那么在分布式的金融核心应用中,谁是发起者呢?肯定不应该是金融核心之外的应用,比如渠道,因为这样的话,分布式这件事对所有渠道都将变得不透明。我们在金融核心的应用架构中设计了一层称为choreography的层次,该层作为统一的发起方。为什么称为choreography,对SOA架构了解的读者应该知道SOA机构中有服务编排的概念,我们巧妙的使用了这一概念,一方面将其作为分布式事务发起方,另一方面也真正使用这一层对下层的微服务,或者说参与者,进行编排,从而形成一个新的端到端的业务。在我们的架构中,一个参与者基本就是一个微服务,所以这种设计同时也对“服务/微服务粒度”这一永恒的问题给出了一种具备可操作性的答案,即一个微服务在一次实际执行中最多只能写一个数据库,所以任何可能对两个数据库进行写操作的服务都需要被拆解。通过这样的规定,其实也变相的可以解决另一个问题,即保证不存在嵌套性事务。


       读到这里,大家应该可以意识到一个问题,即分布式事务并非一个技术框架或者一个分布式事务中间件即可解决的问题,而是需要数据模型,应用架构进行良好设计,来共同达成的。




关注一下,更多精彩等着你!



往期文章:




以上是关于分布式事务——面向新金融的模型化分布式金融核心探秘之四的主要内容,如果未能解决你的问题,请参考以下文章

北京银行基于全分布式架构的核心系统转型实践与展望

阿里云刘伟光:金融核心系统将步入分布式智能化的时代

分布式事务:蚂蚁金服核心金融场景下的演进

数据库系列之金融分布式事务数据库白皮书解读

动态金融分布式事务数据库研讨会在京召开

分布式事务-金融业微服务实践案例