专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
现代快报  ·  74岁刘晓庆官宣 ·  昨天  
现代快报  ·  74岁刘晓庆官宣 ·  昨天  
FM1007福建交通广播  ·  刚刚,破30亿!凌晨2点挤满人…… ·  3 天前  
FM1007福建交通广播  ·  刚刚,破30亿!凌晨2点挤满人…… ·  3 天前  
无锡博报生活  ·  恭喜!无锡这里,身价暴涨! ·  4 天前  
无锡博报生活  ·  恭喜!无锡这里,身价暴涨! ·  4 天前  
51好读  ›  专栏  ›  ImportNew

SpringBoot Seata 死锁问题排查

ImportNew  · 公众号  ·  · 2024-01-22 21:47

正文

(给 ImportNew加星标,提高Java技能)


现象描述:Spring Boot 项目,启动的时候卡住了,一直卡在那里不动,没有报错,也没有日志输出



但是,奇怪的是,本地可以正常启动。



好吧,姑且先不深究为什么本地可以启动而部署到服务器上就无法启动的问题,这个不是重点,重点是怎么让它启动起来。(PS:我猜测可能是环境不同造成的,包括操作系统不同和JDK版本不同)


遇到这种情况,我先用jstack查看堆栈情况,果然发现了死锁。



拿到 jstack 的完整信息,然后仔细排查,看不懂的话也可以借助工具。




分析了每个被阻塞的线程之后,发现 main 线程和 timeoutChecker_1_1 互相等待对方持有的锁,从而形成了死锁


可以通过 jconsole 和 jvisualvm 查看。



需要注意,如果是查看远程进程,则需要加一些启动参数:


  • -Dcom.sun.management.jmxremote:启用 JMX

  • -Dcom.sun.management.jmxremote.port=:指定 JMX 远程连接的端口号

  • -Dcom.sun.management.jmxremote.authenticate=false:禁用 JMX 远程连接的认证

  • -Dcom.sun.management.jmxremote.ssl=false:禁用 JMX 远程连接的 SSL 加密


于是,我又重启启动。


java -jar -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false app.jar

通过 jps 或者 ps 命令查找应用的 pid。







用 jvisualvm 查看也可以,不再赘述,结果都是一样的。




好了,工具介绍到此为止,下面重点看代码



main 线程持有 <0x00000000c07a33d8> 这个对象的锁,同时它还需要 <0x00000000ff295ca8> 对象的锁;而 timeoutChecker_1_1 线程正好相反,于是死锁了


main 线程很好理解,就是我们这个 Spring Boot 应用的主线程。但是timeoutChecker_1_1线程是哪儿来的呢?通过分析发现它来自 Seata。


对了,该项目中 Spring Boot 版本是 2.6.6,Seata 版本是 1.4.2


找到 timeoutChecker 的出处了:





延迟 60 秒启动定时任务,每隔 10 秒执行一次,调用 io.seata.core.rpc.netty.NettyClientChannelManager#reconnect()。



记住这一行,首先调用 RegistryFactory.getInstance() 获取一个 RegistryService,然后调用 RegistryService 对象的 lookup() 方法。




接着看 Seata 1.4.2 的代码。



最重要的是  EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);



所以,ExtConfigurationProviderSpringBootConfigurationProvider





回到 seata-1.4.2,可以看到这里调用了 applicationContext.getBean(),于是 DefaultListableBeanFactory.getBean()。




可以看到,getSingletonFactoryBeanForTypeCheck() 方法里,对 singletonObjects 加了同步锁。


凡是通过 DefaultSingletonBeanRegistry#getSingleton() 获取单例 Bean 的都会先对 singletonObjects 加锁。


接下来看 lookup()



可以看到,NacosRegistryServiceImpl的lookup() 这里也加了锁。


另外,getNamingProperties() 的时候由于再次用到了 ConfigurationFactory.CURRENT_FILE_INSTANCE,所以又到了 SpringBootConfigurationProvider#provide()。


至此,Seata 整个定时任务启动的主要逻辑我们都梳理完了,几处加锁的也都找到了:



这些加锁的地方也就是容易出现死锁的地方


死锁是由于加锁顺序不一致造成的。


下面看 main 线程启动:


由于 SeataDataSourceBeanPostProcessor 实现了 BeanPostProcessor 接口,所以在创建容器之后会回调其 postProcessAfterInitialization() 方法。









所以,最终还是调 NettyClientChannelManager#reconnect()








请到「今天看啥」查看全文