Go开源说第十七期 分布式事务DTM

GoCN

共 4611字,需浏览 10分钟

 · 2021-10-13

大家好,很高兴到“Go开源说”跟大家分享DTM,我是叶东富,DTM作者,github:https://github.com/yedf

内容提纲

本次分享分为以下四个部分:

  1. DTM是什么

  2. 产生的背景

  3. 可以解决什么问题

  4. 发展现状与未来

DTM是什么

我们按照开源项目的习惯,以一个类似Quick Start的例子来说明:

一个例子

业务场景:A跨行转给B 30元,A、B分属不同银行

需求要点:

  • 需要保证A-30和B+30,都成功,或都失败

  • 中间任何一个地方发生故障,不能影响最终的数据一致性

  • 因数据保存在多个数据库实例,无法通过本地事务解决

解决方案DTM:A Distributed Transaction Manager

  req := &gin.H{"amount": 30} // 微服务的载荷  // DtmServer为DTM服务的地址  saga := dtmcli.NewSaga(DtmServer, dtmcli.MustGenGid(DtmServer)).    // 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCompensate"    Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req).    // 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransIn", 逆向操作为url: qsBusi+"/TransInCompensate"    Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req)  // 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务  err := saga.Submit()

在这个例子中:

  1. 定义了一个Saga事务

  2. Saga事务中的第一个子事务TransOut负责扣减A的余额

  3. Saga事务中的第一个子事务TransIn负责增加B的余额

  4. 完成了Saga事务定义之后,将事务提交给DTM

  5. DTM将按顺序完成各项子事务

  6. 如果中间出现临时的网络错误,DTM会负责重试

  7. 如果业务返回失败,DTM会调用子事务的补偿操作,保证最终数据一致性

跨数据源、跨服务的数据一致性

DTM是跨数据源、跨服务的数据一致性解决方案,他又可以分为以下三类:跨数据库、跨服务、混合

易混淆的分布式数据库事务

现在涌现出构建在分布式系统上的数据库--NewSQL,能够随着分布式系统的动态扩容而大幅提高系统的服务能力,例如谷歌的Spanner、国产的TiDB。NewSQL的事务是在分布式系统上构建的,也被称之为分布式事务,但是与我们在前面介绍的分布式事务在解决问题类型不同,所使用的核心技术也不同。

NewSQL事务主要解决的是数据库扩展到多Host下的ACID,聚焦在数据库系统层面,而DTM聚焦在应用层和多数据源之间的数据一致性。

产生的背景

随着我们公司业务的发展,交易系统越来越复杂,交易并发也越来越高。原先一个交易是放在一个本地事务里完成,由数据库来保证ACID。这种架构,一方面要求把所有订单交易的操作在一个服务,导致无法微服务化,这样就会把所有订单涉及的子系统耦合进订单中,复杂度很高;另一方面,订单修改库存到订单提交的这段时间里,库存是被锁定的,购买同一个商品的所有订单会因为扣库存的行锁导致串行,并发度低。

我们系统采用的语言栈是Node,调研之后,决定之后采用Go作为主栈。如果把交易系统全部用Go改写之后再上线,周期十分漫长,对业务非常不友好。我们需要做渐进式的改造,需要分布式事务支持一部分服务Go编写,一部分服务Node编写,然后把相关服务组合成一个全局的分布式事务。

当前可选方案

我们调研了当时市面上的方案,最成熟、应用最广泛的是阿里开源的Seata,它是Java语言开发的,如果我们选用这个方案的话,需要把订单相关的系统,全部改造成Java,工作量巨大,预计要20人月以上。还有其他方案,例如hmily、tcc-transaction、ByteTCC,这些都是Java的方案,工作量都接近。

其他语言的分布式事务方案,没有看到成熟可用的,Go、PHP、Python、Node等语言搜索了一个遍,都未发现好的方案。

加上前面我们希望要一个渐进式的方案,需要支持多语言的子服务组合成一个全局事务,那么市面上的方案就更不可能了。综合评估下来,我们决定自己开发

DTM架构

我们开发的DTM架构如下:


图中左上角的Go客户端SDK等,都是轻量级的SDK,大约几十到几百行代码,新增一门语言支持非常容易。DTM服务器采用云原生友好的无状态服务,可以非常方便的动态扩容、做高可用,数据存储在数据库,和现在绝大多数公司的存储架构一致。

我们支持了类似如下的多语言,能够采用各种语言编写子服务,最终组合成一个全局事务:

可以解决什么问题

DTM提供了跨数据源分布式事务的一站式解决方案,支持的事务模式包括:可靠消息、事务消息、TCC、SAGA、XA。而在这些事务模式中,有一个难点问题是如果中间发生异常,导致子事务乱序要怎么解决。

异常

典型的异常分为三类,:

  • 空补偿:可能出现回滚请求在正向操作前到达,此时需忽略

  • 悬挂:可能出现回滚后,再收到正向操作,此时需忽略

  • 幂等:可能出现重复请求,需要允许操作重试,不影响结果

通俗一点说,转账这类的分布式事务,可能会出现服务先收到撤销转账的请求,然后再收到转账的请求。下面是一个时序图,很好的演示了异常的情况:

其中:

  • 5 Cancel在Try之前被调用,是空补偿

  • 7 Cancel和5的重复,是重复请求

  • 8 Try在Cancel之后执行,是悬挂操作

目前没有看到哪个项目在这方面有成熟好用的解决方案,包括Seata,都要求业务层面直接处理这类问题。业务直接处理这类问题,面临大量逻辑判断,复杂易出错,需要花费大量的时间进行调试测试,而且由于悬挂这样的问题难以模拟,可能要线上跑一段时间后才会出现。业务对于这样的情况只能表示很无奈,很头疼。

子事务屏障

DTM首创了子事务屏障,系统性的解决了上述的异常问题,效果示意图如下:

所有这些请求,到了子事务屏障后:不正常的请求,会被过滤;正常请求,通过屏障。开发者使用子事务屏障之后,前面所说的各种异常全部被妥善处理,业务开发人员只需要关注实际的业务逻辑,负担大大降低。
子事务屏障提供了方法BranchBarrier.Call,方法的原型为:

func (bb *BranchBarrier) Call(db *sql.DB, busiCall BusiFunc) error

业务开发人员,在busiCall里面编写自己的相关逻辑,调用BranchBarrier.Call。BranchBarrier.Call保证,在空回滚、悬挂等场景下,busiCall不会被调用;在业务被重复调用时,有幂等控制,保证只被提交一次。

子事务屏障会管理TCC、SAGA、事务消息等,也可以扩展到其他领域

有了子事务屏障的帮助,分布式事务的使用门槛大大降低,原先需要架构师才能胜任的工作,现在只需要普通开发人员就可以完成。未来DTM有可能成为推进微服务化的核心组件。

说完了所有事务模式共同的问题和解决方案后,我们下面看一下DTM对各个事务模式的支持:

可靠消息、事务消息

这两种事务模式保证消息至少被成功消费一次(或者说相关服务的执行最少成功一次),保证多个微服务被原子执行,它适合不需要回滚的业务场景。例如:

  • 点击按钮,领取优惠券+会员,领优惠券+会员不需要回滚,这种情况适合可靠消息

  • 注册成功后,领取优惠券+会员,这种情况适合事务消息。

第二种情况和第一种对比,差别主要是只有注册成功,才领取,如果注册失败,不可以领取,这种情况适合事务消息

DTM的支持如下:


如果是事务消息,那么在本地事务提交之前调用Prepare,提交之后调用Commit。如果没有本地事务,其实就是可靠消息,忽略Prepare调用即可。

XA

XA是标准二阶段提交,一阶段Prepare,二阶段Commit/Rollback。一阶段的所有Prepare成功,则Commit,否则Rollback。从修改数据开始,到Commit/Rollback结束,锁定数据长,并发度较低。

这种事务模式适合并发不高的订单转账等各项业务。

DTM的支持如下:

TCC

TCC的每个子事务有两个阶段:一阶段Try;二阶段Confirm/Cancel;Try中进行资源预留,例如冻结资金;一阶段如果全部成功,则Confirm,例如进行余额扣减,解冻资金;一阶段如果有一个子事务出现失败,则Cancel,例如解冻资金;

TCC模式不长期锁数据,并发高。常用于支付、订单类业务的拆分。

DTM的支持如下:

SAGA

SAGA每个子事务有正向分支和补偿分支,它在正向分支中直接修改数据,出错则补偿所有修改过的数据。

SAGA比TCC少一个分支,一致性比TCC弱。适用于积分换礼品等不涉及资金业务;也适用于对接较多第三方的长事务。

DTM的支持如下:

未支持的模式AT

AT为SEATA和部分其他的框架提供的事务模式,各方面特性类似于XA,性能比XA高,但低于其他模式。

这种模式的使用需要小心避免脏回滚,假如T1修改Row1,然后Row1被T2直接修改,接下来T1回滚,此时出现脏回滚,需要手动处理。Seata给出避免这种脏回滚的办法是,所有要修改Row1的访问,都必须使用Seata中的Transaction注解,这个要求在多团队协作,新旧系统兼容上难以保证。

因为脏回滚以及类似XA的特性,DTM支持了XA,并未支持AT模式,预计近期也不会支持。

DTM支持的其他场景

DTM还有一些其他典型的适用场景,例如:多个应用间的对账系统、发布文章后更新各类统计信息等

单服务多数据源

DTM对单服务多数据源的支持非常优雅,只需要将每个数据源的修改用一个子事务屏障包装起来即可。如图所示:

发展现状与未来

已完成的特性

  • 从生产系统提炼而来,已接入三家

  • 中英文文档

  • HTTP/gRPC支持

  • 测试覆盖95%以上

  • Promethues监控

  • 完善的CI/CD

  • 支持的语言包括Go、Python、PHP、Node、Java、C#等

  • 完整的线上部署文档,含直接部署、Docker、K8S

行业影响

干货推广文《分布式事务最经典的其中解决方案》被大量的自媒体自发转载(不是主动投稿),大约有几十个公众号,多个自媒体平台(包括脉脉,微博等)。该文章两个月时间就排到谷歌搜索“分布式事务"结果第二。

DTM在Github上面的star增长迅速,受到了很大的关注



下图是DTM和Seata在重要特性上的对比:

可以看到DTM在各项重要特性上不输于,甚至优于第一名,未来的发展潜力很大,他的目标是成为云原生架构的核心组件

结束

我们的项目地址是:https://github.com/yedf/dtm

文档是:https://dtm.pub

大家还可以通过扫码进群,一起交流~



浏览 147
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报