seadt: financial-grade distributed transaction solution - technology selection and implementation

This article was first published on the WeChat public account " Shopee Technical Team " .

Summary

seadt is a distributed transaction solution provided by the Shopee financial product team using Golang for real business scenarios.

Golang currently does not have mature middleware and components to support distributed transactions. How to quickly support business needs and solve distributed transaction problems at the same time is a thorny problem faced by business teams.

This will be a series of articles, divided into chapters. As the opening article, this article will briefly introduce the distributed transaction problems encountered in financial business, and introduce the selection and implementation of the team in detail.

1. Challenges of Distributed Transactions

The Shopee Financial Products team provides financial services such as credit in Southeast Asia and other regions, targeting both C-end and B-end users, and has been launched in multiple markets.

In financial business, the biggest challenge is the very high requirements for data accuracy. When users make loans on the platform, we must ensure that each loan is accurate, neither more nor less. Too much will infringe on the user's property, and if it is too little, it will cause damage to the company's assets. How to ensure the accuracy of the data, in addition to the accuracy of the system calculation algorithm itself, the biggest challenge is the transaction processing caused by the distributed system.

1.1 Distributed Transaction Challenges

For example: a user applies for a loan, and the system returns that the loan application is successful. What will happen to the background system behind it? Let's take a look at the system architecture of Cashloan (non-related partial desensitization):

Many things will happen in Cashloan, such as: freezing coupons, freezing quotas, transferring external payment gateways for lending, waiting for lending results, post-processing of lending results, etc.

These processes are managed and arranged by the transaction module Cashloan-Transaction. Although there is a lot of internal processing and a lot of interfaces, from the user's point of view, the follow-up processing of the loan is a transaction, either the loan succeeds or the loan fails. As for the consistency of the data status between the various systems within the loan success/failure, the transaction module Cashloan-Transaction is required to guarantee.

In the loan business scenario, there are many transaction issues involved: 1) How to freeze coupons and freeze quota to ensure the same status (success and fail at the same time); 2) How to ensure the subsequent processing status of payment gateway success/failure; 3) Others.

本文主要对第一个问题展开讲解,即冻结优惠券和冻结额度如何保证状态一致。后续会有其他文章介绍余下的问题解决方案,欢迎持续关注。

相信很多同学第一时间想到的解决方案是使用 seata。但是我们的开发语言是 Golang,不能直接使用 seata。

于是就有了本篇的技术选型,包括对现有开源的中间件还是自研的选择。当然在此之前,我们团队更加关心选择什么模式更加适合我们的业务。例如 seata 提供了四种模式:TCC 模式、Saga 模式、AT 模式、XA 模式。这些模式都有适用场景,但是第一步需要确定优先级。

1.2 模式选型:TCC

结合项目现状,我们团队做了一些调研分析。

AT 模式 TCC 模式 Saga 模式 现有实现
资料链接 link AT link TCC link Saga 内部文档
原理说明 框架层面记录数据变更前后的镜像,在应用侧做类似 redo、undo 操作 服务提供方提供 TCC 的 2 阶段接口 业务方提供一阶段正向服务,和与之对应的冲正服务 业务逻辑中做到最终一致性,将每一个 RPC 调用的超时/失败/宕机的处理考虑进去,在业务中自实现补偿恢复/回滚等逻辑
适用场景 AT 模式(参考链接)基于支持本地 ACID 事务的关系型数据库:
1) 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录
2) 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志
3) 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚
TCC 模式,不依赖于底层数据资源的事务支持:
1) 一阶段 prepare 行为:调用自定义的 prepare 逻辑
2) 二阶段 commit 行为:调用自定义的 commit 逻辑
3) 二阶段 rollback 行为:调用自定义的 rollback 逻辑

所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中
1) 业务流程长、业务流程多
2) 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
简单的业务场景,只有本地和一次 RPC 写调用
优势 对业务无侵入 1) 一阶段提交本地事务,无锁,高性能
2) 扩展性好,增加一个参与者无额外开发成本
3) 易理解
4) 对外屏蔽复杂性
1) 一阶段提交本地事务,无锁,高性能
2) 事件驱动架构,参与者可异步执行,高吞吐
3) 补偿服务易于实现
简单易懂
不足 1) 保证隔离性,但是锁的时间长
2) 实现较为复杂,严重依赖数据库,而且需要根据数据库事务类型做不同处理,容易出错
1) 对业务有侵入,需要提供中间状态
2) 对业务开发要求高
1) 不保证隔离性
2) 对业务存在一定入侵
1) 不易维护,扩展性差
2) 每个场景处理都不一样,可复制性低

1.2.1 AT 模式

贷款流程引入 AT 模式流程如下:

虽然该方式对业务无入侵,影响点最小。但是该模式资源锁的时间长,对于中间并发可能带来灾难性后果,并且实现难度大。出现问题排查及恢复困难,团队也没有该方面的经验和知识储备。

因此不选择 AT 模式。

1.2.2 Saga 模式

贷款流程引入 Saga 模式交互如下:

业务流程长,对于各业务节点要求高,需要支持回滚接口,而回滚接口业务上能否支持存疑。例如:贷款中如果使用该模式,优惠券/资金方的回退能否支持未知(可能存在长时间跨度的回退,例如支付通过后的处理本身就存在跨多日),并且其他附带业务的金额回退比较麻烦,需走线下回退给用户。

因此先不支持 Saga 模式。

1.2.3 业务自实现

贷款流程当前业务实现逻辑如下:

针对第一步“扣减额度处理”展开说明,其时序如下:

说明:为了保证额度扣减与整个用户操作的状态一致性(即用户贷款成功额度才会冻结扣减,用户贷款未成功,则额度最终未冻结未扣减),做了 ①~⑦ 处理。

其中:

  • ① 在还未冻结额度前就添加延时队列执行恢复额度操作,是为了避免 ② 冻结额度调用后应用宕机,额度冻结无法解冻的特殊处理;
  • ③ 在额度冻结成功后,则需要将恢复冻结额度的延时队列删除,避免恢复冻结额度而导致用户贷款成功而额度未扣减;
  • ⑥ 利用可靠事件保证最终一定执行并成功。

由此可见,异常场景的处理融合在业务代码中,导致业务流程特别复杂,并且无法扩展和复用。例如贷款中的实现,在还款场景下,不能完全照搬过去,依然需要考虑还款的特殊性。在贷款场景中,如果添加其他外部调用,需要重新考虑整个流程的一致性,如上图,只实现了冻结额度的一致性处理,加入其他业务处理,不能按照同样的方式实现。

因此不选择业务自实现。

1.2.4 TCC 模式

贷款流程引入 TCC 模式流程如下(同 AT):

对业务存在侵入,对开发水平要求高,需要考虑几种异常情况并处理。好在这几种异常情况有统一解决模板,降低问题出现概率。

因此使用 TCC 模式。

1.3 技术选型:自研

模式确定后,就考虑如何技术选型了。我们做了 4 种对比:seata-golang、seata-go-server、内部其他系统实践(不对外公开对比)、自研。

调研过程中结合了很多维度进行考量:维护状态、支持团队、社区建设、成熟度、文档、功能性、License、可维护性、集成性、团队意向等方面。

以下分析基于 2021-6-25 调研数据。

1.3.1 seata-go-server

  • 维护状态:停止更新(上次更新 2019-7-10)
  • 成熟度:无 release 版本
  • 文档:无

已经 2 年未更新,因此不考虑。

1.3.2 seata-golang

  • 维护状态:维护中,更新较少(最近活跃起来,有持续更新)
  • 成熟度:无 release 版本
  • 支持团队:目前仅一人
  • 文档:无有效文档,均为 Java 版 seata 的文档
  • 功能性:仅支持 TCC 模式(12 月份后支持 Saga 模式)
  • 集成性:集成了 ORM 框架 go-sql-driver/mysql,与本团队当前使用的 ORM 冲突
  • License:Apache License 2.0
  • 开发模式:
    • 原分支开发:与 seata 团队共同开发,对需要改的内容发 merge 请求,需 alibaba/opentrx 团队审核。
    • 新分支开发:需要托管在 GitHub 的 opentrx 代码仓库下,由于开发权限无法控制,可能会被他人更改。好处:opentrx 团队的变更较方便的同步到分支中。
    • 新仓库:独立一套,放在内网 GitLab 中。劣势:opentrx 的更改无法同步更新;opentrx 的很多预留功能点难以理解,改动较难。

虽然持续更新中,但是没有稳定的 release 版本,也没有任何的商业应用,Cashloan 当前业务量大,直接使用风险高。因此也不考虑。

1.3.3 自研

  • 维护状态:项目不倒,维护不停
  • 支持团队:至少 3 人
  • 文档:目前已有丰富设计文档,内部分享文档,还会持续建设
  • 功能性:优先支持 TCC 模式,再支持 Saga 模式
  • 集成性:基于金融团队技术栈开发,无集成问题
  • 团队意向:团队意向高,可以解决部门内大量分布式场景问题

Cashloan 在各市场一直受强监管约束。例如,数据库敏感信息加密、日志脱敏等。外部团队为此做特殊版本较难。Cashloan 的代码托管如果在外部仓库,监管方面风险大。

自研可针对上述的问题设计支持,缺点则需要额外投入人力从零开始设计和开发,并且上线之后可能会遇到很多问题。

可行性分析:

  • 技术:团队技术储备雄厚,对 TCC 熟练掌握,有类似框架组件的开发经验(去年本团队做了可靠事件组件且上线后大量场景应用);
  • 应用:团队作为业务团队,本身有大量业务场景可以应用,可以保证可落地;
  • 投入产出:投入产出比可观。团队在实现贷款流程一致性中,投入了 1 人/月设计和开发联调,后续半年在贷款需求变更中,陆陆续续投入 2 人/月。类似场景光本团队就有 5 处之多。而自研开发人力投入 2 人/月左右,改造对接联调 0.25 人/月场景,后续业务无维护成本。

除此之外,自研有如下好处:

  • 解决 Cashloan 自身业务问题:放款/还款流程等;
  • 提升系统可维护性、可扩展性;
  • 符合适用原则,满足当前业务需求;
  • 符合简单原则,第一阶段的实现够简单,清晰明了,确保团队内成员能够接受理解,提升可维护性;
  • 符合演化原则,预留好其他功能的演化迭代扩展,便于日后业务变更带来的需求挑战;
  • 可作为能力输出,给整个金融团队带来效率提升,解决分布式事务共性问题。

2. seadt-TCC 设计与实现

既然已经确定自研 TCC,首先就要考虑 TCC 的架构设计。有两种设计方案,一是纯 SDK 模式(SDK 模式),另一种是 SDK+独立中央服务(TC 全局模式)。

这两种有何区别,先看看分布式事务组件的结构:

  • TM(Transaction Manager):客户端 SDK,开启/结束分布式事务;
  • RM(Resource Manager):客户端 SDK,管理本地资源;
  • TC(Transaction Coordinator):客户端 SDK 或者服务端,管理全局事务及分支事务状态,以及推进事务执行。

SDK 模式:TM 和 TC 在一起。

TC 全局模式:TC 单独作为服务部署。

考虑到未来使用嵌套事务,方便统一监控管理,以及 Saga 模式支持等,我们选择了 TC 全局模式。

全局模式的交互如下:

由业务方 Transaction 模块启动分布式事务,由 TC 与各模块交互,推进整个事务往下执行。

2.1 Cashloan 新系统架构

引入 seadt 后的整体架构如下:

每个业务系统模块按需引入 seadt-SDK(使用分布式事务),所有的系统可以公用同一套 TC 服务。

各个业务系统模块引入 seadt-SDK 后依然可以水平扩展,同时也支持分库分表。

seadt-TC 作为公共服务,很容易成为新的瓶颈,因此做了高可用设计,可以水平扩展、分库分表,允许多租户模式公用,也允许各业务进行物理隔离部署。

业务系统模块引入 seadt-SDK 结构如下(以示例中的 Transaction 模块为例):

Transaction 模块引入 seadt-SDK,SDK 包含事务管理器 TM 和资源管理器 RM,同时还包含可靠事件管理器 Reliable_Event(本地消息方式保证最终一致性)。

2.2 TC 全局模式

TC 全局模式,对 TCC 的支持:

  • 发起者 TM 向 TC 注册全局事务;
  • 发起者冻结额度和冻结优惠券;
  • 参与者 RM 注册分支事务;
  • 参与者执行一阶段 Try 方法,做优惠券冻结业务处理;
  • 发起者 RM 执行本地业务处理;
  • 发起者 TM 提交全局事务;
  • TC 执行二阶段 Confirm/Cancel;
  • 参与者 RM 执行二阶段 Confirm/Cancel 方法,做真正的业务处理。

TC 全局模式,对 Saga 的支持(本期暂不详细介绍):

2.3 状态机设计

分布式事务中有两个核心的状态机,主事务状态机、分支事务状态机。

Main Transaction State Machine Diagram

Branch Transaction State Machine Diagram

TM 与 RM 状态矩阵(行代表主事务,列代表分支事务):

分支\主 Prepared Committing Committed Rollbacking Rollbacked
- Y N N Y N
Prepared N Y N N Y N
Tried N Y Y N Y N
Confirmed N N Y Y N N
Canceled N N N N Y Y

注意:这里有个特殊的情况,即主事务在 Rollbacking 状态,分支事务可能在 Prepared 状态。

对应的场景为:TM 向多个参与者中发送一阶段 T 请求的时候,如果有一个业务执行报错则分支状态会停留在 Prepared 状态,而 TM 收到 T 失败处理后立即进入 Rollback 流程,同时向所有参与者立即广播二阶段 Cancel 处理,所以会出现主事务在 Rollbacking 状态,分支事务在 Prepared 状态。

在这个场景下,由于多个参与者处理速度和结果不一样,会同时存在无数据、Prepared、Tried,以及 Canceled 状态。

该特殊场景,也会体现在 RM 与 RM 之间,其中一个为 Canceled,另一个无数据(空回滚)、Prepared、Tried、Canceled。但是绝对不可能存在一个 Canceled,另一个为 Confirmed。

RM 与 RM 状态矩阵:

分支\分支 Prepared Tried Confirmed Canceled
Y Y Y N Y
Prepared Y Y Y N Y
Tried Y Y Y Y Y
Confirmed N N Y Y N
Canceled Y Y Y N Y

2.4 详细流程

2.4.1 术语说明

  • Commit、Rollback:整个分布式事务的状态以及分支事务的状态。
  • Confirm、Cancel:只有在调用参与者接口的时候会使用 Confirm、Cancel 表述。
  • commit、rollback:底层代码具体的事务操作方法。

2.4.2 业务 TCC 处理

seadt 的 TCC 模式设计目标,就是业务处理中外部调用能像本地事务一样简单。使用 seadt 后,业务处理如下:

2.4.3 SDK 中 TCC 的 Commit 处理

1)先看业务启动分布式事务,SDK 中的处理:

SDK 提供了一个全新的事务模板 SDK-TT,会注册事务触发器,该事务触发器贯穿整个 TCC 事务。事务模板中事务触发器详情见 2.4.5 事务模板。

2)业务调外部 Try 接口,SDK 内部实现。

3)业务走 Commit 流程,SDK 内部实现。

特殊说明,上图中的 tx-global 代表的业务开启的分布式事务,4.1.3 Activity 置为 Commit,需要同业务开启的分布式事务在一个事务内。如果是开启新事务 tx-sub,则有可能全局事务状态为Commit,但是后面出现异常,导致实际走的是 rollback 流程。

seadt-SDK 将分布式问题统一处理,让业务代码依然能够保持像本地事务处理一样简单。

各类异常处理均由 seadt-SDK 实现,例如:

  • 主事务、分支事务状态维护;
  • Commit 流程的 rollback 处理;
  • 二阶段的推进处理。

2.4.4 SDK 中 TCC 的 Rollback 处理

1)如果发生异常进入 Rollback 流程,SDK 的处理。

说明:启动分布式事务发生异常,会进入 rollback 流程。调用外部 Try 方法异常/超时会进入 rollback 流程。

2)Rollback 的 SDK 处理。

2.4.5 SDK 中 TCC 的事务恢复处理

事务恢复管理器会定时触发,将分布式事务已经确定 Commit/Rollback,而分支事务未进入终态的进行补偿处理。处理如下:

2.4.6 事务模板

无论上面的 Commit 流程处理还是 Rollback 流程处理,都依赖事务模板的各类触发器,而这个也是 Golang 事务模板未提供的,因此我们重新设计了一个新的事务模板,其触发器设计如下:

这个事务模板及它包含的各类事务触发器,才是 seadt-SDK 的基石。

2.4.7 注意事项

分布式事务在进入 Commit 前,任何异常报错都进入 Rollback 流程。Commit 流程中红框部分是往往忽略的点,虽然参与者都 Try 成功,并且业务代码准备 commit 事务,但是在 seadt-SDK 内部依然存在红框部分执行失败,导致整个分布式事务最终走向 Rollback。此后发生失败,则由事务恢复处理器补偿处理。

Rollback 流程中,大部分报错都可以简化到由事务恢复处理器补偿处理。为了分布式事务快速失败,因此做了立即触发调用参与者 Cancel 处理。

需要区分 Commit 的 commit 和 rollback,Rollback 的 commit 和 rollback。

2.5 seadt 约束与规范

seadt 的 TCC 模式,采用的是 2PC 思想提交事务,需要满足原子承诺协议(atomic commitment protocol)。参考该协议,seadt 也提出了自身的一些协议规范,确保事务流转高效可控。

AC1: All participants that decide reach the same decision.
AC2: If any participant decides COMMIT, then all participants must have voted YES.
AC3: If all participants vote YES and no failures occur, then all participants decide COMMIT. 
AC4: Each participant decides at most once (that is, a decision is irreversible).
					
A protocol that satisfies all four of the above properties is called an atomic commitment protocol.

—— 引自:Ozalp Babaoglu. Understanding Non-Blocking Atomic Commitment. January 1993
复制代码

2.5.1 发起者约束与规范

  • 全局事务状态只能由发起者决定;
  • 所有参与者一阶段成功才能进入 Commit;
  • 提供分布式事务反查接口;
  • 启动分布式事务,需要考虑自身是否为嵌套事务,SDK 是否支持。

说明:

  • seadt 的 TCC 模式,定位为 Blocking Atomic Commitment,只能由发起者 TM 决定事务状态,不允许参与者决定;
  • 所有参与者 Try 执行成功后事务才能进入 Commit 流程。有一个参与者失败则进入 Rollback 流程;
  • SDK 提供统一的事务反查接口,发起者无需实现。由于存在发起者本地事务提交,未通知 TC 宕机的情况。TC 不允许决策事务最终状态,因此只能反查 TM;
  • 当前不支持嵌套事务,因此不允许使用嵌套事务。

2.5.2 参与者约束与规范

  • 实现 TCC 接口,Try 锁资源,CC 确保业务上一定能成功;
  • 控制幂等、并发处理;
  • 禁止空提交,允许空回滚;
  • 避免事务悬挂,并做好监控告警;
  • 做好数据可见性和隔离性;
  • 二阶段处理中不允许作为发起者发起分布式事务。

说明:

  • 参与者必须在 Try 阶段就将所有的资源占用,否则 TC 在推进 Confirm 的时候,就无法成功;
  • TC 通知参与者二阶段,无法保证 Exactly Once,只能做到 At Least Once,因此需要幂等。由于存在重试以及网络延时等情况,也会存在并发情况;
  • 不会存在参与者 Try 未执行,TC 通知 Confirm。允许存在 Try 未执行,Cancel 先到的情况;
  • 发生空回滚后,如果 Try 才到,如果未做特殊处理,则发生事务悬挂,资源无法释放。seadt-SDK 中统一处理;
  • 数据可见性和隔离性需要业务自身处理,例如余额增加冻结额度。

2.5.3 TC 约束与规范

  • TC 不可确定分布式事务状态;
  • 二阶段状态不可变,在 TC 落地的二阶段状态就是终态,无论什么情况都不允许改变;
  • 确保分支事务二阶段成功;
  • 数据清理及归档;
  • 超时失败的事务或者长期悬挂在一阶段的事务,需要告警。

说明:

  • TC 在长时间未收到 TM 通知,也不允许决策事务状态,会反查 TM 拿到事务结果;
  • TC 推进参与者执行二阶段,即便多次重试依然报错,也不允许调整事务状态。而是报错告警,人工干预;
  • TC 通过广播形式保证参与者二阶段执行成功;
  • 分布式事务结束后,及时数据清理,避免数据堆积;Rollback 状态事务长期保留,Commit 状态事务保留一定时间,定时批量清除;
  • TC 拥有全局事务及分支事务数据和状态,因此可以监控长时间悬挂事务。

2.6 seadt 难点分析

  • Golang 如何实现 AOP 切面功能,使得参与者的分支事务注册、事务结果上报、幂等控制、空回滚、事务悬挂处理等可以在 seadt-SDK 中统一处理;
  • TC 调用 RM 的二阶段,如何解决 TC 不依赖 RM 的 pb,以及如何能够反射调到参与者真正的业务二阶段方法;
  • TC 的高可用设计。

这些难点的解决方案,会在接下来的文章中介绍。

目前 seadt 组件已经在部分核心业务流程中得到使用,大大减少了原有业务自实现中的开发内容。未来 seadt 还将支持 Saga 模式,让业务团队在事务处理中更加轻松自如。

后续我们将针对 seadt 的应用、新功能、新规划以及难点设计等输出文档说明,大家敬请期待。

本文作者

Marshal、Ansen、Yongchang,来自 Financial Products 团队。

Guess you like

Origin juejin.im/post/7086704002313748488