Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AT和TCC模式混合使用场景的问题咨询 #7163

Open
wxrqforever opened this issue Feb 13, 2025 · 28 comments
Open

AT和TCC模式混合使用场景的问题咨询 #7163

wxrqforever opened this issue Feb 13, 2025 · 28 comments

Comments

@wxrqforever
Copy link
Contributor

一、背景介绍
有三个应用A、B、C,A、B能够接入seata,C为python开发无法seata。应用A提供了两个接口a1、a2,a1的调用链路为A、B因此使用的是AT模式,a2的调用链为A、B、C,因此使用的AT+Tcc模式。a2方法简化的相关代码如下:

//应用A的a2方法
@GloableTransactional
public void a2(){
    saveDb();
    //rpc访问b2的prepare方法
    b2Prepare();
    //假设抛出异常
    throw exception;
}

@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "b2Commit", rollbackMethod = "b2Cancel")
public void b2Prepare(){
    //rpc访问B的b2接口
    call B.b2();
}

public void b2Commit(){
    //不关键忽略
}


public void b2Cancel(){
    //rpc访问B的rollbackB2接口
    call B.rollbackB2();
}

//应用B的b2接口
public void b2(){
   //模拟一些自己的db处理
    saveDb();
}

public void rollbackB2(){
   //模拟一些自己的db处理
   cleanDb();
}

我遇到的问题是在应用A中,b2Prepare方法通过rpc访问B的b2接口时候,全局的事务的头信息会被传递到B中,b2中的saveDb方法会以at模式运行,会生成前后镜像,也会注册at分支。然后我的疑问是,如果这时候在a2中抛出了异常,那么对于at的部分b2中saveDb的数据会被清除,tcc部分rollbackB2方法也会被调用,这对于B而言相当于执行了两次回滚,这是符合预期的吗是否在tcc分支下应该抑制全局事务消息的传递?

@wxrqforever
Copy link
Contributor Author

@funky-eyes 哥 这个问题帮忙看看 我之前描述的有点啰嗦了 就是发现tcc模式下,在prepare接口调用第三方服务时也会把全局事务头带过去,这如果第三方服务也接入seata,那会导致第三方同时以tcc、At两种模式加入全局事务,此时如果回滚的话,at模式的回滚和cancel回滚都会被调用。这是符合预期的设计吗?是否可以考虑tcc模式下应该抑制全局事务消息的传递?

@funky-eyes
Copy link
Contributor

When TCC and AT use the same data source, the @GlobalLock annotation must be added.

@wxrqforever
Copy link
Contributor Author

When TCC and AT use the same data source, the @GlobalLock annotation must be added.

我在下游添加了@GlobalLock注解,但是在上游来看 下游仍然同时是以at和tcc两种模式一起运行:

这是下游at的分支注册:
2025-02-24 11:38:33.863 INFO org.apache.seata.rm.AbstractResourceManager [http-nio-7713-exec-1] [] [] branch register success, xid:xxxx:36632205101172404, branchId:36632205101172437, lockKeys:xxxx:76

这是上游tcc的分支注册和commit方法被调用成功的记录:
2025-02-24 11:37:02.039 INFO xxx.TccPerformanceTestServiceImpl [rpcDispatch_RMROLE_1_1_20] [] [] commit iscalled
2025-02-24 11:37:02.040 INFO org.apache.seata.rm.AbstractResourceManager [rpcDispatch_RMROLE_1_1_20] [] [] TCC resource commit result : true, xid: xxxx:36632205101172349, branchId: 36632205101172376, resourceId:xxxx

这里我理解@GlobalLock是被设计出来在非全局事务范围进行锁冲突的校验,似乎不能改变分支的模式,我这里是希望下游只以tcc模式运行,我目前是采用的方案prepare方法中rpc下游前,临时退出全局事务

String xid = RootContext.unbind();
testSeataApi.testSeata();
RootContext.bind(xid);

但是这似乎成了每次tcc调用下游必须做的事,按我目前理解,tcc模式应该就是分支的终点,全局事务的信息就不应该继续往下传播了。

@wangliang181230
Copy link
Contributor

接入TCC的方法,不会被AT代理的,所以只会创建TCC分支,没有AT分支。执行的b2Prepare方法,最多只会本地事务回滚,不会被AT回滚。

@wangliang181230
Copy link
Contributor

源代码见:SeataAutoDataSourceProxyAdvice

Image

@wxrqforever
Copy link
Contributor Author

接入TCC的方法,不会被AT代理的,所以只会创建TCC分支,没有AT分支。执行的b2Prepare方法,最多只会本地事务回滚,不会被AT回滚。

是的 上游发起tcc的应用,tcc中的prepare方法里的执行是不会被at代理。但是我这里的主要疑问是,如果我在上游tcc的prepare方法中通过feign rpc下游应用,这时候头信息会被带到下游,下游会加入到at模式中运行,这时下游同时被at模式和tcc所控制,比如如果发生全局事务的回滚,对于下游则同时运行在at模式的回滚和tcc的cancel方法下。

@funky-eyes
Copy link
Contributor

我明白了,你是在一个tcc分支内去远程调用了一个AT分支是吧?所以你的cancel里就会又去调用AT的远程分支,那是不是你把cancel里的调用远程的AT分支代码去掉就好了?因为远程分支自己会回滚。

@wxrqforever
Copy link
Contributor Author

我明白了,你是在一个tcc分支内去远程调用了一个AT分支是吧?所以你的cancel里就会又去调用AT的远程分支,那是不是你把cancel里的调用远程的AT分支代码去掉就好了?因为远程分支自己会回滚。

对!差不多是这个意思,但是问题在于,并不是我去调用了一个at分支,而是远程调用默认下游就以at模式加入了,目前头信息里也是不带有分支类型的吧。另外这里之所以用tcc去调用,就是因为下游无法完全以at模式运作,具体来说就是下游的下游是python写的,所以这里通过tcc方式接入,在cancel方法里是要调用下游提供的回滚接口,光以at回滚会导致数据不一致。

@funky-eyes
Copy link
Contributor

下游是python写的,为什么会加入at模式?

@wxrqforever
Copy link
Contributor Author

下游是python写的,为什么会加入at模式?

是下游的下游是python,比如A->B->C这样的调用链,C是python写的,然后现在是B基于C对外提供了一些能力,提供了正向计算和回滚的接口。从A这里发起调用,就是以TCC的方式分别在prepare和cancel中调用B的计算和回滚接口。

@funky-eyes
Copy link
Contributor

可以将顺序改动一下,比如先执行AT分支,然后让调用python服务的方法单独成为一个tcc分支,不要将代码都写在tcc的 prepare中

@wxrqforever
Copy link
Contributor Author

可以将顺序改动一下,比如先执行AT分支,然后让调用python服务的方法单独成为一个tcc分支,不要将代码都写在tcc的 prepare中

还是以A->B->C(Python)这样的调用链为例,我理解想表达的就是让原本是由A发起的TCC,改为在B发起TCC(调用C的部分)。但是这么做有两个问题,一是在使用上有些不直观,如果这么用的话相当于于有个隐藏约束,如果要在tcc发起rpc,调用的接口的下游不能包含有写db操作,这里其实对B而言下游C,C是python写的,所以可以,如果C也是java,这就意味着还需要其他业务逻辑的调整,调整到不包括写操作为止。另一个,即使改为在B发起TCC,如果未来C做了重构,变成了JAVA并且接入seata,则相对应B这里也需要改动,这不太利于维护,很难察觉到有这层关系在。

总的来说,既然在tcc模式下,发起tcc的应用中比如prepare方法里,at模式是不会生效的,这里不就隐含着tcc模式的发起过程中,其实at模式是不应该生效的,那其实正在去定义tcc范围,下游调用的接口也应该在这个范围内,为什么不能考虑在组件传播上事务信息时,考虑当前的分支模式,如果是at才继续传播。

Image

@funky-eyes
Copy link
Contributor

funky-eyes commented Feb 25, 2025

我举个简单例子

@GloableTransactional
public void demo(){
    saveDb();
    //rpc访问b2的方法
    b2();
    //rpc访问c3的prepare方法
    c3();
    //假设抛出异常
    throw exception;
}
@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "c3Commit", rollbackMethod = "c3Cancel")
public void c3(){
    //rpc访问B的b3接口
    call c3 python应用接口();
}

或者:

//应用A的a2方法
@GloableTransactional
public void a2(){
    saveDb();
    //rpc访问b2的方法
    b2();
    //假设抛出异常
    throw exception;
}

//应用B
public void b2(){
    b2savedb();
    c2prepare();
}


@Transactional
public void b2savedb(){
    saveDb();
}

@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "c3Commit", rollbackMethod = "c3Cancel")
public void c3(){
    call c3 python应用接口();
}

public void c3Commit(){
    call c3 python应用接口();
}
public void c3Cancel(){
    call c3 python应用接口();
}

@wxrqforever
Copy link
Contributor Author

我举个简单例子

@GloableTransactional
public void demo(){
    saveDb();
    //rpc访问b2的方法
    b2();
    //rpc访问c3的prepare方法
    c3();
    //假设抛出异常
    throw exception;
}
@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "c3Commit", rollbackMethod = "c3Cancel")
public void c3(){
    //rpc访问B的b3接口
    call c3 python应用接口();
}

或者:

//应用A的a2方法
@GloableTransactional
public void a2(){
    saveDb();
    //rpc访问b2的方法
    b2();
    //假设抛出异常
    throw exception;
}

//应用B
public void b2(){
    b2savedb();
    c2prepare();
}


@Transactional
public void b2savedb(){
    saveDb();
}

@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "c3Commit", rollbackMethod = "c3Cancel")
public void c3(){
    call c3 python应用接口();
}

public void c3Commit(){
    call c3 python应用接口();
}
public void c3Cancel(){
    call c3 python应用接口();
}

辛苦老哥答疑,是这样的你举的两个例子中,其中例子1在我们的场景下由于服务按职责分层,是不能直接访问c3接口的,而是要访问b2(),b2提供了完整的服务(他在内部会调用c3)。但是如果对b2发起tcc就会有我说的,应用B被同时加入到了tcc和at分支下。

例子2中,其实就是由b发起tcc,这么做现在是ok的,但是有个隐患,如果未来c切换成java,那么b2就得改,否则就是应用c被同时加入到tcc和at分支下。

@funky-eyes
Copy link
Contributor

就算以后c变成java了,依然可以用tcc,除非你不准备用tcc了才需要改代码。
并且tcc的使用规范就是将注解放到provider上,而不是consumer侧,你这个例子放到了调用方也就是consumer侧就是不正确使用方式。并且tcc本身就是预留资源,释放资源和提交资源,你不应该在tcc中执行两个不是一个原子性的动作,你可以将2个动作拆解成2个分支,就比如我所说的这样,这样每个动作才是单一的,互不影响的。

@wxrqforever
Copy link
Contributor Author

就算以后c变成java了,依然可以用tcc,除非你不准备用tcc了才需要改代码。 并且tcc的使用规范就是将注解放到provider上,而不是consumer侧,你这个例子放到了调用方也就是consumer侧就是不正确使用方式。并且tcc本身就是预留资源,释放资源和提交资源,你不应该在tcc中执行两个不是一个原子性的动作,你可以将2个动作拆解成2个分支,就比如我所说的这样,这样每个动作才是单一的,互不影响的。

我理解这不仅仅是使用规不规范的问题,即使放在provider上并且都是原子性动作仍然会有问题,以c从python到java来说明。

现状:c现在是python,现在假设tcc的注解是放在了b上,就是放在provider上,也就是下面这个写法。然后我们再假设,call c3 python应用接口1()、 call c3 python应用接口3();都是原子的,假设c3是订单业务为例

//应用b 
@TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "c3Commit", rollbackMethod = "c3Cancel")
public void c3(){
   //以订单业务为例
  //插入订单表一条数据,状态未待确认
    call c3 python应用接口1();
}
public void c3Commit(){
  //更新状态为已待确认
    call c3 python应用接口2();
}
public void c3Cancel(){
    //删除订单信息
    call c3 python应用接口3();
}

未来:如果c变成了java,并且接入seata,那么上面原先c3方法(prepare)再次被调用时,通过call c3 python应用接口1();会将全局事务头信息带到c的接口里,c的接口执行时里会认为当前是处于全局事务的at模式下,执行“插入订单表一条数据,状态未待确认”操作时,会注册at模式分支,如果此时全局回滚,对b而言会call c3 python应用接口1();去“删除订单信息”,而c注册的at模式也会回滚这条插入订单的数据。

这样不就重复执行了回滚吗?

@funky-eyes
Copy link
Contributor

tcc要单一原则,怎么能在下面又有远程调用又有业务逻辑?

@wxrqforever
Copy link
Contributor Author

tcc要单一原则,怎么能在下面又有远程调用又有业务逻辑?

就是单一的“插入订单表一条数据,状态未待确认” 这些注释是指的call c3 python应用接口1();这个远程调用做的事

@funky-eyes
Copy link
Contributor

那你只要换成java的时候,提供一个v2版本接口给服务B即可,理论上接口迭代都应该有版本,或者分组等信息来做灰度,再逐步全量换为新版本的接口。所以根据正常的业务需求迭代和架构规范,不可能在原接口上,直接从python换java,即便要换,也应该是java的先不用AT模式,依旧以tcc运行,先发布第一版,后续再迭代第二版接口,也就是@TwoPhaseBusinessAction从B直接到C这个应用

@wxrqforever
Copy link
Contributor Author

那你只要换成java的时候,提供一个v2版本接口给服务B即可,理论上接口迭代都应该有版本,或者分组等信息来做灰度,再逐步全量换为新版本的接口。所以根据正常的业务需求迭代和架构规范,不可能在原接口上,直接从python换java,即便要换,也应该是java的先不用AT模式,依旧以tcc运行,先发布第一版,后续再迭代第二版接口,也就是@TwoPhaseBusinessAction从B直接到C这个应用

差异就在这,当切成java后提供的v2版本接口时,实际c业务逻辑做的事是和python一样的,仍是提供三个接口这是没变的,但是C却需要增加tcc的注解。现实来看A、B、C都是不同组甚至是不同部门的人管,很难保证他在切成java后能够评估到这个接口需要增加这些注解这件事,这无形增加了依赖,很难感知到这点。

而且从这个过程中可以发现,因为在tcc中能够向下游传递事务头信息,所以tcc提供者必须在最下游(java调用链的最下游)

@wxrqforever
Copy link
Contributor Author

那你只要换成java的时候,提供一个v2版本接口给服务B即可,理论上接口迭代都应该有版本,或者分组等信息来做灰度,再逐步全量换为新版本的接口。所以根据正常的业务需求迭代和架构规范,不可能在原接口上,直接从python换java,即便要换,也应该是java的先不用AT模式,依旧以tcc运行,先发布第一版,后续再迭代第二版接口,也就是@TwoPhaseBusinessAction从B直接到C这个应用

我无法理解为什么在tcc中rpc下游需要传递xid信息,首先在tcc方法执行过程中at模式是不生效的,另外tcc中的try-cancel是对应的,cancel就是负责回滚try的处理。我没明白是什么场景需要将通过tcc将事务消息带到下游,让下游也加入到全局事务中。

@funky-eyes
Copy link
Contributor

你在try阶段做了N个事情,这已经脱离了tcc范畴了。

@funky-eyes
Copy link
Contributor

那你只要换成java的时候,提供一个v2版本接口给服务B即可,理论上接口迭代都应该有版本,或者分组等信息来做灰度,再逐步全量换为新版本的接口。所以根据正常的业务需求迭代和架构规范,不可能在原接口上,直接从python换java,即便要换,也应该是java的先不用AT模式,依旧以tcc运行,先发布第一版,后续再迭代第二版接口,也就是@TwoPhaseBusinessAction从B直接到C这个应用

差异就在这,当切成java后提供的v2版本接口时,实际c业务逻辑做的事是和python一样的,仍是提供三个接口这是没变的,但是C却需要增加tcc的注解。现实来看A、B、C都是不同组甚至是不同部门的人管,很难保证他在切成java后能够评估到这个接口需要增加这些注解这件事,这无形增加了依赖,很难感知到这点。

而且从这个过程中可以发现,因为在tcc中能够向下游传递事务头信息,所以tcc提供者必须在最下游(java调用链的最下游)

就是因为每个应用有不同的负责,只有对应应用的负责团队才应该决定是否使用tcc,还是at,而不是调用方来决定

@wxrqforever
Copy link
Contributor Author

你在try阶段做了N个事情,这已经脱离了tcc范畴了。

已上面的例子为例,c提供的是python接口只做往db插入一行订单记录的事,而b在tcc只做rpc c一件事,并没有做了n件事啊。

@wxrqforever
Copy link
Contributor Author

你在try阶段做了N个事情,这已经脱离了tcc范畴了。

按我目前的理解,现在相当于有个约束,如果你在tcc方法中rpc第三方接口(第三方也接入seata),那么第三方接口的逻辑中就不能包含有写db的操作。

@wxrqforever
Copy link
Contributor Author

你在try阶段做了N个事情,这已经脱离了tcc范畴了。

老哥 感觉我们已经聊了好几天了,是不是可以升级一下沟通方式,提高一下效率,是否可以通过腾讯会议、或者其他软件会议的手段进一步聊一下这个问题。

@funky-eyes
Copy link
Contributor

我不认为这是个问题,或者是个bug,纯粹是使用方式不正确而已。如果按你说的B只做了rpc到c,未来c变成java应用也没有影响,因为可以通过不同接口和版本的方式来迭代从python到java的迭代过程。
tcc在执行过程中将xid携带到下游是没问题的,只要你cancel的时候不要处理那个本身就能回滚的下游节点不就好了?并且TwoPhaseBusinessAction要放到提供者侧,你放到消费者侧本身就是错误的

@wangliang181230
Copy link
Contributor

正确的使用方式,只在B调C时,用TCC,C提供prepare、cancel、commit三个接口,对应让B的TCC模式的三个方法去调C的三个接口。

B都接入了seata了,上游服务A在调用B时就不要用TCC。用TCC就是重复提交或回滚了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants