Workflow 模式是 github.com/dtm-labs/dtm 独创推出的模式,在这个模式下,能够混合应用 XA、SAGA、TCC,也能够混合应用 HTTP、gRPC,用户能够对分布式事务外面的绝大部分内容进行定制,具备极大的灵活性,上面咱们以转账场景,讲述如何在 Workflow 下进行实现。
workflow 例子
Workflow 模式下,既能够应用 HTTP 协定,也能够应用 gRPC 协定,上面以 gRPC 协定作为例子,一共分为一下几步:
- 初始化 SDK
- 注册 workflow
- 执行 workflow
首先须要在应用 workflow 前对 SDK 的 Workflow 进行初始化:
import "github.com/dtm-labs/dtmgrpc/workflow"
// 初始化 workflow SDK,三个参数别离为:// 第一个参数,dtm 服务器地址
// 第二个参数,业务服务器地址
// 第三个参数,grpcServer
// workflow 的须要从 "业务服务器地址"+"grpcServer" 上接管 dtm 服务器的回调
workflow.InitGrpc(dtmGrpcServer, busi.BusiGrpc, gsvr)
而后须要注册 workflow 的处理函数
wfName := "wf_saga"
err := workflow.Register(wfName, func(wf *workflow.Workflow, data []byte) error {req := MustUnmarshalReqGrpc(data)
wf.NewBranch().OnRollback(func(bb *dtmcli.BranchBarrier) error {_, err := busi.BusiCli.TransOutRevert(wf.Context, req)
return err
})
_, err := busi.BusiCli.TransOut(wf.Context, req)
if err != nil {return err}
wf.NewBranch().OnRollback(func(bb *dtmcli.BranchBarrier) error {_, err := busi.BusiCli.TransInRevert(wf.Context, req)
return err
})
_, err = busi.BusiCli.TransIn(wf.Context, req)
return err
})
- 这个注册操作须要在业务服务启动之后执行,因为当过程 crash,dtm 会回调业务服务器,持续未实现的工作
- 上述代码
NewBranch
会创立一个事务分支,一个分支会包含一个正向操作,以及全局事务提交 / 回滚时的回调 OnRollback/OnCommit
会给以后事务分支指定全局事务回滚 / 提交时的回调,上述代码中,只指定了OnRollback
,属于 Saga 模式-
这外面的
busi.BusiCli
须要增加 workflow 的拦截器,该拦截器会主动把 rpc 的申请后果记录到 dtm,如下所示conn1, err := grpc.Dial(busi.BusiGrpc, grpc.WithUnaryInterceptor(workflow.Interceptor), nossl) busi.BusiCli = busi.NewBusiClient(conn1)
当然您也能够给所有的 gRPC client 增加 workflow.Interceptor
,这个中间件只会解决wf.Context
和wf.NewBranchContext()
下的申请
- 当工作流函数返回 nil/ErrFailure,全局事务会进入 Commit/Rollback 阶段,反序调用函数外部 OnCommit/OnRollback 注册的操作
最初是执行 workflow
req := &busi.ReqGrpc{Amount: 30}
err = workflow.Execute(wfName, shortuuid.New(), dtmgimp.MustProtoMarshal(req))
- 当 Execute 的后果为
nil/ErrFailure
时,全局事务已胜利 / 已回滚。 - 当 Execute 的后果为其余值时,dtm 服务器后续会回调这个工作流工作进行重试
workflow 原理
workflow 是如何保障分布式事务的数据一致性呢?当业务过程呈现 crash 等问题时,dtm 服务器会发现这个 workflow 全局事务超时未实现,那么 dtm 会采纳指数回避的策略,对 workflow 事务进行重试。当 workflow 的重试申请达到业务服务,SDK 会从 dtm 服务器读取全局事务的进度,对于已实现的分支,会将之前保留的后果,通过 gRPC/HTTP 等拦截器,间接返回分支后果。最终 workflow 会顺利完成。
工作流函数须要做到幂等,即第一次调用,或者后续重试,都该当取得同样的后果
Workflow 下的 Saga
Saga 模式源自于这篇论文 SAGAS,其核心思想是将长事务拆分为多个短事务,由 Saga 事务协调器协调,如果每个短事务都胜利提交实现,那么全局事务就失常实现,如果某个步骤失败,则依据相同程序一次调用弥补操作。
在 Workflow 模式下,您能够在函数中,间接调用正向操作的函数,而后将弥补操作写到分支的OnRollback
,那么弥补操作就会主动被调用,达到了 Saga 模式的成果
Workflow 下的 Tcc
Tcc 模式源自于这篇论文 Life beyond Distributed Transactions:an Apostate’s Opinion,他将一个大事务分成多个小事务,每个小事务有三个操作:
- Try 阶段:尝试执行,实现所有业务查看(一致性), 预留必须业务资源(准隔离性)
- Confirm 阶段:如果所有分支的 Try 都胜利了,则走到 Confirm 阶段。Confirm 真正执行业务,不作任何业务查看,只应用 Try 阶段预留的业务资源
- Cancel 阶段:如果所有分支的 Try 有一个失败了,则走到 Cancel 阶段。Cancel 开释 Try 阶段预留的业务资源。
对于咱们的 A 跨行转账给 B 的场景,如果采纳 SAGA,在正向操作中调余额,在弥补操作中,反向调整余额,那
么会呈现以下状况:
- A 扣款胜利
- A 看到余额缩小,并通知 B
- 金额转入 B 失败,整个事务回滚
- B 始终收不到这笔资金
这样给 AB 单方带来了极大的困扰。这种状况在 SAGA 中无奈防止,然而能够通过 TCC 来解决,设计技巧如下:
- 在账户中的 balance 字段之外,再引入一个 trading_balance 字段
- Try 阶段查看账户是否被解冻,查看账户余额是否短缺,没问题后,调整 trading_balance(即业务上的冻结资金)
- Confirm 阶段,调整 balance,调整 trading_balance(即业务上的冻结资金)
- Cancel 阶段,调整 trading_balance(即业务上的冻结资金)
这种状况下,一旦终端用户 A 看到本人的余额扣减了,那么 B 肯定可能收到资金
在 Workflow 模式下,您能够在函数中,间接调用 Try
操作,而后将 Confirm
操作写到分支的 OnCommit
,将Cancel
操作写到分支的 OnRollback
,达到了Tcc
模式的成果
Workflow 下的 XA
XA 是由 X /Open 组织提出的分布式事务的标准,XA 标准次要定义了 (全局) 事务管理器 (TM) 和(部分)资源管理器 (RM) 之间的接口。本地的数据库如 mysql 在 XA 中表演的是 RM 角色
XA 一共分为两阶段:
第一阶段(prepare):即所有的参与者 RM 筹备执行事务并锁住须要的资源。参与者 ready 时,向 TM 报告已准备就绪。
第二阶段 (commit/rollback):当事务管理者 (TM) 确认所有参与者 (RM) 都 ready 后,向所有参与者发送 commit 命令。
目前支流的数据库根本都反对 XA 事务,包含 mysql、oracle、sqlserver、postgre
在 Workflow 模式下,你能够在工作流函数中,调用 NewBranch().DoXa
来开启您的 XA 事务分支。
多种模式混合应用
在 Workflow 模式下,上述的 Saga、Tcc、XA 都是分支事务的模式,因而能够局部分支采纳一种模式,其余分支采纳另一种模式。这种混合模式带来的灵活性能够做到依据分支事务的个性抉择子模式,因而倡议如下:
- XA:如果业务没有行锁争抢,那么能够采纳 XA,这个模式须要的额定开发量比拟低,
Commit/Rollback
是数据库主动实现的。例如这个模式适宜创立订单业务,不同的订单锁定的订单行不同,相互之间并发无影响;不适宜扣减库存,因为波及同一个商品的订单都会争抢这个商品的行锁,会导致并发度低。 - Saga:不适宜 XA 的一般业务能够采纳这个模式,这个模式额定的开发量比 Tcc 要少,只须要开发正向操作和弥补操作
- Tcc:适宜一致性要求较高,例如后面介绍的转账,这个模式额定的开发量最多,须要开发包含
Try/Confirm/Cancel
幂等要求
在 Workflow 模式下,当 crash 产生时,会进行重试,因而要求各个操作反对幂等,即第屡次调用和一次调用的后果是一样的,返回雷同的后果。业务中,通常采纳数据库的 unique key
来实现幂等,具体为insert ignore "unique-key"
,如果插入失败,阐明这个操作已实现,此次间接疏忽返回;如果插入胜利,阐明这是首次操作,持续后续的业务操作。
如果您的业务自身就是幂等的,那么您间接操作您的业务即可;如果您的业务为提供幂等性能,那么 dtm 提供了 BranchBarrier
辅助类,基于上述 unique-key 原理,能够不便的帮忙开发者实现在 Mysql/Mongo/Redis
中实现幂等操作。
以下两个是典型的非幂等操作,请留神:
- 超时回滚:如果您的业务中有一个操作可能耗时长,并且您想要让您的全局事务在期待超时后,返回失败,进行回滚。那么这个就不是幂等操作,因为在极其状况下,两个过程同时调用了该操作,一个返回了超时失败,而另一个返回了胜利,导致后果不同
- 达到重试下限后回滚:剖析过程同上。
Workflow 模式暂未反对上述的超时回滚及重试达到下限后回滚,如果您有相干的场景需要,欢送把具体场景给咱们,咱们将踊跃思考是否增加这种的反对
分支操作后果
分支操作会返回以下几种后果:
- 胜利:分支操作返回
HTTP-200/gRPC-nil
- 业务失败:分支操作返回
HTTP-409/gRPC-Aborted
,不再重试,全局事务须要进行回滚 - 进行中:分支操作返回
HTTP-425/gRPC-FailPrecondition
,这个后果示意事务正在失常进行中,要求 dtm 重试时,不要采纳指数退却算法,而是采纳固定距离重试 - 未知谬误:分支操作返回其余后果,示意未知谬误,dtm 会重试这个工作流,采纳指数退却算法
如果您的现有服务与上述的后果不同,那么您能够通过 workflow.Options.HTTPResp2DtmError/GRPCError2DtmError
来定制这部分后果
Saga 的弥补操作、Tcc 的 Confirm/Cancel 操作,依照 Saga 和 Tcc 的协定,是不容许返回业务上的失败,因为到了工作流的第二阶段 Commit/Rollback,此时既不胜利,也不让重试,那么全局事务无奈实现,这点请开发者在设计时就要留神防止
事务实现告诉
局部业务场景,想要取得事务实现的告诉,这个性能能够通过在第一个事务分支上设置 OnFinish
回调来实现。当回调函数被调用时,所有的业务操作曾经执行结束,因而全局事务在本质上曾经实现。回调函数能够根据传入的 isCommit
来判断全局事务最终提交了还是回滚了。
有一个中央须要留神,收到 OnFinish
回调时,dtm 服务器上,这个事务的状态还未修改为最终状态,因而如果混合应用事务实现告诉和查问全局事务后果,那么两者的后果可能不统一,倡议用户只应用其中一种形式,而不要混合应用。
性能
在 DTM 里,失常实现一个 Workflow 事务,须要两个离开的全局事务写(一个是 Prepare 时保留,另一个是将状态改为胜利),须要保留两头事务进度(这部分批量化后,开销很小)。比照 DTM 的 Saga 模式,少了一个独自的分支事务保留,另外分支事务的写入质变小(胜利的事务不须要额定保留弥补分支),因而性能会比 Saga 的性能更好,具体的测试报告,将来会出。
下一步工作
- 逐步完善 workflow 的例子以及文档
- 反对分支事务并发
分割咱们
欢送拜访咱们的我的项目,并 star 反对咱们:
https://github.com/dtm-labs/dtm
关注【分布式事务】公众号,取得更多分布式事务相干常识