http_workflow_tcc_barrier 某种异常情况下,子事务 rollback 之后又会走到 commit 步骤
zhenlanghuo opened this issue · 26 comments
我发现了 dtm-examples http_workflow_tcc_barrier 例子中的一个异常情况的问题
复现步骤如下
- 执行 go run main.go http_workflow_tcc_barrier
- TccBTransOutTry 执行成功 (branch_id 01)
- 这个时候branch_id 01在 barrier 表中有一条记录为
- TccBTransInTry 执行失败 (brand_id 02)
- 紧接着 workflow 流程会执行 branch_id 01 的 rollback 操作,这个时候branch_id 01在 barrier 表中有两条记录
- 这个时候停掉程序模拟程序崩溃,branch_id 01的回滚结果甚至都没有注册到dtm服务器
- 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功
- 执行 go run main.go http_workflow_tcc_barrier,快速模拟dtm发现全局事务超时回调业务服务器重跑事务的过程额
- 这个时候,TccBTransOutTry 返回执行成功 (实际没有执行,barrier给拦住了,但是没有返回错误)
- 然后 TccBTransInTry 执行成功,这个时候 brand_id 02 在 barrier 表中有一条记录
- 紧接着 workflow 流程会执行 branch_id 02 的 commit 操作,这个时候 branch_id 02 在 barrier 表中有两条记录
- 再紧接着 workflow 流程会执行 branch_id 01 的 commit 操作,这个时候 branch_id 01 在 barrier 表中有三条记录
也就是 TccBTransOut 分支在 rollback 的情况下,没有重新try的情况,又走到了 commit 的状态,这显然是不正确的
我认为 barrier 需要判断 try 操作之前是执行成功还是被 rollback 了,如果是执行成功就返回成功,如果是被 rollback 了就返回失败,这样重试的逻辑才正确
dtm-examples 的go mod如下
module github.com/dtm-labs/dtm-examples
go 1.15
require (
github.com/dtm-labs/client v1.15.1
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis/v8 v8.11.5
github.com/go-resty/resty/v2 v2.7.0
github.com/go-sql-driver/mysql v1.6.0
github.com/lib/pq v1.10.3
github.com/lithammer/shortuuid/v3 v3.0.7
go.mongodb.org/mongo-driver v1.9.1
google.golang.org/grpc v1.47.0
google.golang.org/protobuf v1.28.0
gorm.io/driver/mysql v1.0.3
gorm.io/driver/postgres v1.2.1
gorm.io/gorm v1.22.2
)
// replace github.com/dtm-labs/client/dtmcli => /Users/wangxi/dtm/dtmcli
// replace github.com/dtm-labs/client/dtmgrpc => /Users/wangxi/dtm/dtmgrpc
- TccBTransInTry 执行失败 (brand_id 02)
- 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功
作为分布式应用,需要做到幂等,这个是基本要求。4和7的行为不是幂等行为,因此属于你的业务程序设计问题,而不是dtm的行为。如果业务不幂等,无论dtm怎么做,都无法保证数据一致
- TccBTransInTry 执行失败 (brand_id 02)
- 然后修改 http_workflow_tcc_barrier 的代码:1) 修改gid为上一次执行的gid;2) 设置 TccBTransInTry 执行成功
作为分布式应用,需要做到幂等,这个是基本要求。4和7的行为不是幂等行为,因此属于你的业务程序设计问题,而不是dtm的行为。如果业务不幂等,无论dtm怎么做,都无法保证数据一致
- 使用了barrier不应该就是由barrier来保证幂等吗
- 4和7的行为为什么不是幂等,4的失败可能是网络超时失败,实际执行成功的,所以再次执行,7返回成功,这样不是幂等吗
- 子事务都回滚了,再次执行子事务的try,返回成功,难道这是正确的吗
使用了barrier不应该就是由barrier来保证幂等吗
是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了
4和7的行为为什么不是幂等,4的失败可能是网络超时失败,实际执行成功的,所以再次执行,7返回成功,这样不是幂等吗
网络超时失败,跟子事务失败回滚完全不同,dtm会重试网络失败,而不是把事务分支记录为失败,
子事务都回滚了,再次执行子事务的try,返回成功,难道这是正确的吗
这个行为不是dtm自发行为,是中间错误的修改了子事务的逻辑造成的
是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了
假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit
通过以上的方式手动模拟金币不足返回错误的情况
TccBTransInTry失败了(金币不足)
这个事情发生了之后,返回ResultFailure,就不会再返回成功了,即使金币足够了,也只会返回barrier保存的失败
TccBTransInTry失败了(金币不足)
这个事情发生了之后,返回ResultFailure,就不会再返回成功了,即使金币足够了,也只会返回barrier保存的失败
@yedf2 不啊,返回ResultFailure之后,barrier不会保存失败,是rollback的
这样吧,你给个复现了问题的可运行的代码例子,而不是你手动的debug改数据,这样描述的问题最清晰最准确,就能够弄清楚怎么回事了
这样吧,你给个复现了问题的可运行的代码例子,而不是你手动的debug改数据,这样描述的问题最清晰最准确,就能够弄清楚怎么回事了
@yedf2 我在dtm-examples上写了一下可以复现问题的代码例子,但是没有权限push分支(从main分支拉出来的分支),无法跟你这边交流
是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了
假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit
通过以上的方式手动模拟金币不足返回错误的情况
barrier和业务逻辑用同一个sql.Tx,就不会出现你说的这种情况
是的,barrier可以保证幂等,但是你在barrier记录失败之后,又设置TccBTransInTry 执行成功,这就矛盾了,barrier记录失败之后,永远不再会返回成功了
假设 TccBTransInTry 是一个扣金币的动作,TccBTransOutTry 成功了,TccBTransInTry失败了(金币不足),然后TccBTransOutRollback,在submit全局事务之前,程序崩溃了(用debug调试运行,在submit全局事务之前退出)。之后重启程序,dtm服务器那边resume,这个时候会重新调用 TccBTransOutTry , barrier会直接返回成功(不会真正执行TccBTransOutTry),TccBTransInTry 此时也成功(此时金币足够了),之后就会执行 TccBTransOutCommit 和 TccBTransInCommit,相当于 TccBTransIn既执行了rollback也执行了commit
通过以上的方式手动模拟金币不足返回错误的情况
@yedf2barrier和业务逻辑用同一个sql.Tx,就不会出现你说的这种情况
@relxet 这个写法 barrier和业务逻辑 就是 用同一个sql.Tx 呀
TccBTransOutRollback 在崩溃前,执行成功了没有?如果成功了,dtm恢复以后是直接走rollback流程,而不是像你说的,重新从try再走一遍流程
而且try是客户端触发的,try再执行一次,就是一个新的事务
TccBTransOutRollback 在崩溃前,执行成功了没有?如果成功了,dtm恢复以后是直接走rollback流程,而不是像你说的,重新从try再走一遍流程
TccBTransOutRollback执行成功之后,业务服务器崩溃,全局事务未到终态,dtm服务器会不断得调用resume来重试,业务服务器恢复之后,resume在业务服务器中执行的顺序是从头开始(用之前的全局事务id),调用TccBTransOutTry,此时虽然该全局事务下该事务分支已经有了rollback的记录,但是barrier是返回的成功,所以接下来会继续执行TccBTransInTry,如果这个时候TccBTransInTry执行成功,就会进入到commit的流程
你搞错了,try是ap端直接call的,tm不会去call try,何来从头开始。ap重新call try,就要注册一个新的gid,就是一个新事物了。
你搞错了,try是ap端直接call的,tm不会去call try,何来从头开始。ap重新call try,就要注册一个新的gid,就是一个新事物了。
@relxet
你有尝试过resume吗,resume是带着原来的全局事务gid,重新跑的
我说的重新跑,是指在ap端重新跑
你一个事务分支的rollback都执行过了,还要让ap从try再跑一遍?你这流程就不是dtm的流程。你就要让dtm把当前这个事务rollback完,ap端重新发起一次新的事务,而不是原来的事务重新从try跑一遍。
你一个事务分支的rollback都执行过了,还要让ap从try再跑一遍?你这流程就不是dtm的流程。你就要让dtm把当前这个事务rollback完,ap端重新发起一次新的事务,而不是原来的事务重新从try跑一遍。
@relxet
这个是workflow这个模式的执行机制啊,不是我定的啊,你有看懂workflow的执行机制吗
tcc协议中定义为commit和rollback必须成功,你不能重新从try跑一遍事务
tcc协议中定义为commit和rollback必须成功,你不能重新从try跑一遍事务
@relxet 你先好好去看下workflow是怎么跑的,如果中间出错了,是怎么重试的
这跟workflow没关系,workflow也是几个协议的组合,saga的补偿操作,Tcc的Confirm/Cancel操作,协议上规定,不能允许失败。
这跟workflow没关系,workflow也是几个协议的组合,saga的补偿操作,Tcc的Confirm/Cancel操作,协议上规定,不能允许失败。
- 我这里说的就是workflow模式下执行tcc在某些异常场景下有问题,这个问题跟workflow的执行机制有关系,你撇开workflow来说tcc,有啥用
- dtm通过resume接口访问业务服务器,让业务服务器继续完成全局事务,官网上是这么写的
而我说的业务服务器会重新跑一遍,workflow的process函数就是这么干的,会用原来的gid重新调用TccBTransOutTry,TccBTransOutTry接口通过barrier做幂等、空补偿、防悬挂,barrier在分支事务存在rollback记录的情况下,再次调用try会返回成功,之后就导致我说的情况,所以这里面是更多的是workflow模式的重试机制或者是barrier的返回问题
@zhenlanghuo 你不需要push到dtm-examples下面,你只需要在你自己的github账号下面弄好了例子和说明,我就能够直接下载复现问题了
@zhenlanghuo 你不需要push到dtm-examples下面,你只需要在你自己的github账号下面弄好了例子和说明,我就能够直接下载复现问题了
@yedf2
我这边找到原因了,之前dtm-examples引用的dtm的版本是v1.15.1,这个版本workflow存在问题:Workflow.getProgress返回的[]*dtmgpb.DtmProgress永远是一个空数组,具体原因是生成的DtmProgressesReply结构体的json tag的字段名是大写开头的,这个在最新版本中已经修复了
@relxet
我这边找到原因了,之前dtm-examples引用的dtm的版本是v1.15.1,这个版本workflow存在问题,导致wf.progresses没有正确设置,所以每次resume都会重新调用TccBTransOutTry