前言
近期新开发的服务上线后,用户反馈数据更新不成功;但经过本地测试又是正常的;考虑到本地和线上环境的区别是一个单体一个集群。考虑到这个因素,我在本地又起了一个服务,测试结果是大概率的操作失败,事务没有提交成功;由于选择的框架目前已无人维护所以只能开启debug模式来排查问题,经过两天时间的排查终于发现是TM根据模块名称找参与者造成的问题,由于框架的模块名称取值逻辑是采用项目名称,集群下的服务注册到TM时模块名称是一样的,则TM找寻不到具体的参与者,造成了事务提交失败的。
1、版本
2、流程分析
依据下图的TC代理控制处理的流程图,从源码层次来分析集群下分布式事务失效的原因。
[
从上图可知,事务的提交最终交由TM来控制,因此TM最终会通知参与方来响应事务;但在集群的环境下真正的参与方大概率没有接受到TM传来的消息;所以我们的切入点自然是TM的通知信息发送这个过程。
2.1 TM的NotifyGroup的源码分析
TM通知TC的过程
1 | java复制代码private void notifyTransaction(DTXContext dtxContext, int transactionState) throws TransactionException { |
- @@1 获取该事务组的所有参与方
- @@2 根据参与方标识获取连接
- @@3 向第一个连接发送消息
从@@2和@@3 可知,如果集群下是不是modId一样造成了TM找不到准确的TC,带着这个问题我们看看@@2 的处理逻辑:
1 | java复制代码 |
- @@1 依据moduleName获取TC地址
- @@2 通过遍历所有与TM建立的连接,依据moduleName查找符合条件的连接
- @@3 依据channel得到其ModuleName
- @@4 依据远程地址得到ModuleName,其中AppInfo的结构是CurrentHashMap
从上述过程可知transUnit.getModId()和AppInfo 是我们排查的重点;通过其相互的调用关系,最终确定了两者初始化的地方。
AppInfo的初始化
1 | java复制代码//TM端的InitClientService |
TransactionUnit的初始化
1 | java复制代码//JoinGroupExecuteService |
从上述源码可知TransactionUnit的ModId 最终传的AppName,而AppName则是environment.getProperty(“spring.application.name”)得来的。从中可知,集群下同一服务的spring.application.name是相同的。
3、结论
集群下由于应用名称是一样的,则造成了TM端发送通知信息时找不到准确的参与方,进而导致了事务提交失败;鉴于此种情况一种就是改应用服务名,启动一次服务改一次服务名,此种方法繁琐不切实际故舍去;第二种就是修改源码改变查找逻辑,将TransactionUnit的ModId改为labelName,其中LableName 的命名为服务名+ip+端口号,故需重写相关方法。
4、 解决方案
修改源码两种方式:
- 直接下载源码在源码中修改,修改完成后打包使用
- 在项目下创建同目录同文件的类,依据编译文件的优先级来达到修改源码的效果。
本次解决方案会采用第二种方式,第一种方式耦合度高,且不宜框架版本升级。
4.1、重写modId的方法
将该文件放在项目的公共模块类中
1 | java复制代码@Configuration |
4.2 重写SocketManager类
注:在项目的公共包中建立同目录同类的文件
1 | java复制代码/* |
5、总结
本人写作能力水平有限,文中相关的讲解有误请帮忙指出,谢谢!下一步的计划分析tx-lcn的框架,了解其内部原理,从中加强对分布式事务的理解。
本文转载自: 掘金