一、项目背景
在项目的发展过程中,我们将整个新交易平台(业务平台部专门为360集团内部打造的类似有赞、微盟的交易系统)微服务化,产生了店铺服务、商品服务、订单服务、优惠券服务、红包服务、用户服务、支付服务、履约服务、售后服务等等。并且这些服务由不同的语言开发。
当前端的用户提交订单,服务端需要完成以下操作:
创建订单:需要在订单表中创建订单,唯一键为订单ID
扣减库存:需要给用户下单的商品扣减库存
核销优惠券:用户在下单前,选择了可使用的优惠券,提交订单时,则扣减这部分优惠券
扣减红包:用户在下单前,选择了可使用的红包,提交订单时,则扣减这部分红包金额
扣减积分:用户在下单前,如果有积分余额,提交订单时,则扣减这部分积分
创建支付单:提交订单后,需要创建支付单,最终供用户支付
对于上述这个场景,如果在单体订单系统中,很容易使用数据库的事务来解决。但是一旦微服务化了之后,由于这些操作分布在不同的服务中间,则需要按顺序依次去调用各个过程。在这个过程中就会遇到许多进程故障、某一操作无法完成需回滚、重复请求等问题。因此我们就要考虑分布式场景下的事务一致性解决方案。
二、分布式方案选型
由于订单创建的过程中是需要回滚的,所以首先排除了消息通知类(消息通知、本地事务消息等)的模式。排除之后发现仅剩XA、TCC、SAGA,下面我们分析一下这三种事务模式
XA
XA协议最初由Tuxedo首先提出,后被提交给X/Open组织作为资源管理器(数据库)与事务管理器的接口标准。XA规范主要定义了全局事务管理器(TM)和局部资源管理器(RM)之间的接口。在XA中,本地的数据库扮演的是RM角色。目前,主流的数据库基本都支持XA事务,包括MySQL、Oracle、SQL Server和PostgreSQL。
XA一共分为两阶段:
第一阶段(prepare):事务管理器(TM)会向所有参与者发送prepare消息,询问它们是否可以提交事务
第二阶段 (commit/rollback):如果所有参与者都能够提交,则事务管理器(TM)向它们发送commit消息,让它们提交事务。如果有任何一个参与者不能提交,则协调器向它们发送rollback消息,让它们回滚事务。
通过这两个阶段操作,使所有的参与者数据都能保持一致性,但XA事务模式虽然能够保证分布式事务的一致性,但是由于是数据库层面的资源锁定,会带来一些性能上的开销。排除此模式
TCC
TCC是Try、Confirm、Cancel三个词语的缩写,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC分为3个阶段
Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
Confirm 阶段:如果所有分支的Try都成功了,则走到Confirm阶段。Confirm真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源
Cancel 阶段:如果所有分支的Try有一个失败了,则走到Cancel阶段。Cancel释放 Try 阶段预留的业务资源。
TCC特点如下:
并发度较高,无长期资源锁定
开发量较大,需要提供Try/Confirm/Cancel接口
一致性较好,不会有暴漏给用户待支付订单,最后又被取消的情况
TCC适用于订单类业务,对中间状态有约束的业务
但是由于我们系统存在历史逻辑,短时间内无法完全重构以支持资源预留,所以也排除了此模式
SAGA
SAGA最初出现在1987年Hector Garcaa-Molrna & Kenneth Salem发表的论文SAGAS里。其核心思想是将长事务拆分为多个短事务,由Saga事务协调器协调,如果每个短事务都成功提交完成,那么全局事务就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga分为2个阶段
Action阶段:正向执行,无需做资源预留
Compensate阶段:如果某一个过程出错,调用补偿接口,依次进行资源逆向补偿
Saga事务的特点:
并发度高,不用像XA事务那样长期锁定资源
需要定义正常操作以及补偿操作,开发量比XA大,但比TCC小
一致性较弱,对于订单创建,有可能出现待支付订单,最后又被取消的情况
由于Saga仅需要在发生异常需回滚时提供一个逆向补偿的接口,开发量较少,且几乎不用对历史业务逻辑进行改动。故采用此方案而接受短暂数据不一致的问题。
三、SAGA模式&DTM在创建订单过程中的实践
1、DTM分布式事务管理器的确认
确定了Saga模式,接着我们就要进行实施落地,如果自研分布式事务管理器有很大的开发工作,于是去寻找有没有符合我们实际需求的开源工具,最终确定两款比较成熟的开源系统,JAVA SEATA&&Golang DTM,由于seata仅支持Dubbo、Spring Cloud等协议,并且除java版本SDK外,其他语言的SDK完善度不高。DTM支持http、grpc协议。最终确定使用DTM。
接入DTM后的下单时序如下所示:
首先看看,下单 api 的主要处理过程:
上面的代码首先创建了一个SAGA事务,然后添加了多个子事务,每个事务分支包括action和compensate两个操作,分别为Add函数的第一第二个参数。子事务定好之后提交给dtm。dtm收到saga提交的全局事务后,会调用所有子事务的正向操作,如果所有正向操作成功完成,那么事务成功结束。如果有正向操作失败,例如账户库存不足,那么dtm会调用各分支的补偿操作,进行回滚,最后事务成功回滚。
进程crash问题 dtm的saga事务进行过程中,如果发生进程crash,那么dtm会进行重试,保证操作会最终完成
回滚问题 上述这个saga事务中,如果扣减库存时发现库存不足,则返回failure,会进行回滚。dtm 会记录哪些操作已完成,并回滚相关的操作
但是由于分布式事务的引入,因为网络的时序无法保证,会引入幂等、空回滚、悬挂等新问题。
2、解决重复请求、空回滚、悬挂等问题
分布式事务之所以难,主要是因为分布式系统中的各个节点都可能发生各种非预期的情况。分布式系统最大的敌人可能就是NPC了,在这里它是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:
Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
Process Pause,进程暂停。当基于某些需要,例如内存垃圾回收、CPU 排队、服务迁移等,某服务会暂时暂停。
Clock Drift,时钟漂移。分布式系统涉及大量的服务器,而不同服务器通常使用 NTP (Network Time Protocol)协议将本地设备的时间与时间服务器对齐对齐后,通常会导致本地时间跳跃。
分布式事务既然是分布式的系统,自然也有NPC问题。因为没有涉及时间戳,带来的困扰主要是NP。我们以扣减库存为例,看看NP带来的影响
3、网络异常状态下库存扣减问题
一般情况下,一个SAGA异步补偿时的执行顺序是,先执行完库存扣减,再执行库存回滚,但是由于N,则有可能库存扣减的网络延迟大,导致先执行库存回滚,再执行库存扣减。
这种情况就引入了分布式事务中的两个难题:
空补偿:compensate执行时,action未执行,事务分支的compensate操作需要判断出action未执行,这时需要忽略compensate中的业务数据更新,直接返回
悬挂:action执行时,compensate已执行完成,事务分支的action操作需要判断出compensate已执行,这时需要忽略action中的业务数据更新,直接返回
分布式事务还有一类需要处理的常见问题,就是重复请求
幂等: 由于任何一个请求都可能出现网络异常,出现重复请求,所有的分布式事务分支操作,都需要保证幂等性
上图中,库存扣减操作超时而执行的库存回滚,若在商品服务执行成功,但反馈的结果由于 NPC 问题不能到达事务调度器,那么事务调度器还有可能再次发送库存回滚。这就意味着商品服务的库存回滚操作会被多次重复调用。我们必须保证分布式事务的全部操作分支保证幂等性。也就是重复调用操作分支,但不会产生叠加的影响。
不论空补偿、悬挂还是幂等,都需要在业务逻辑层面做出判定。通常的做法是通过分布式事务事件日志的方案来标识操作状态,进而决定是否需要处理空补偿和防止悬挂。
4、DTM解决方案在库存扣减服务中的实现
如果没有一个通用的操作方法去解决幂等、悬挂、空回滚等问题,系统也面临着大量的开发工作,并且每个业务都要仔细处理这三张问题。好在看到了DTM的子事务屏障技术。
子事务屏障技术的原理是,在本地数据库,建立分支操作状态表dtm_barrier,唯一键为全局事务id-分支id-分支操作
开启本地事务
对于当前操作op(action|compensate),insert ignore一条数据gid-branchid-op,如果插入不成功,提交事务返回成功(常见的幂等控制方法)
如果当前操作是compensate,那么在insert ignore一条数据gid-branchid-action,如果插入成功(注意是成功),则提交事务返回成功
调用屏障内的业务逻辑,如果业务返回成功,则提交事务返回成功;如果业务返回失败,则回滚事务返回失败
由于我们的库存服务使用的是java语言,DTM中java版本SDK还不支持SAGA事务的子事务屏障,于是我们使用此原理自己研发
在此机制下,我们看一下怎么解决乱序相关的问题
空补偿控制:如果action没有执行,直接执行了compensate,那么插入gid-branchid-action会成功,不走屏障内的逻辑,保证了空补偿控制
幂等控制:gid-branchid-{action/compensate}在任何一个操作都无法重复插入唯一键,保证了不会重复执行
防悬挂控制:action在compensate之后执行,由于执行compensate时会在插入gid-branchid-action,导致action请求插入gid-branchid-action不成功,就不执行屏障内的逻辑,保证了防悬挂控制
总结
伴随着业务的快速地发展、越来越高的业务复杂度,几乎每个公司的系统都会从单体走向分布式,特别是转向微服务架构。分布式事务本身就是一个技术难题,如果没有合适的框架、工具,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作。经过我们调研和探索,很好地利用DTM事务管理器与系统结合。将分布式事务相关逻辑全部交由 DTM处理,而让我们相应的开发者更聚焦于业务本身,只需要安心写好相关操作和补偿操作即可。