Spring Data Jpa Sping Boot 应用程序的@PostConstruct方法中的死锁

5rgfhyps  于 2023-11-19  发布在  Spring
关注(0)|答案(1)|浏览(171)

我正在使用Spring TaskList在应用程序启动时调度任务(显然......)。
在我的SpringConfig中创建了TaskTable:

@Configuration
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

}

字符串
spring Boot 应用程序在我的Main.class中启动,并在PostConstruct上调度任务

@SpringBootApplication
@ComponentScan("...")
@EntityScan("...")
@EnableJpaRepositories("... .repositories")
@EnableAutoConfiguration
@PropertySources(value = {@PropertySource("classpath:application.properties")})
public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static SpringApplication application = new SpringApplication(Main.class);
    
    private TaskScheduler taskScheduler;

    private AnalysisCleaningThread cleaningThread;

    @Inject
    public void setCleaningThread(AnalysisCleaningThread cleaningThread) {
        this.cleaningThread = cleaningThread;
    }

    @Inject
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public static void main(String[] args)
            throws Exception {

        try {

            //Do some setup

            application.run(args);

        } catch (Exception e) {

            LOGGER.error(e.getMessage(), e);
        }
    }

    @PostConstruct
    public void init()
            throws Exception {

        //Do some setup as well

        ScheduledFuture scheduledFuture = null;
        LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********");
        Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant());
        scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds);



        while (true) {
           //Somehow blocks thread from running
           if (scheduledFuture.isDone()) {
               break;
           }
           Thread.sleep(2000);
        }


        //schedule next periodic thread
    
}


应用程序必须等待线程完成,因为它的任务是在应用程序意外关闭后清理脏数据库条目。下一个任务是拾取清理后的条目并再次处理它们。清理线程的实现方式如下:

@Named
@Singleton
public class AnalysisCleaningThread implements Runnable {

    private static Logger LOGGER = LoggerFactory.getLogger(AnalysisCleaningThread.class);

    private AnalysisService analysisService;

    @Inject
    public void setAnalysisService(AnalysisService analysisService) {
        this.analysisService = analysisService;
    }

    @Override
    public void run() {
        List<Analysis> dirtyAnalyses = analysisService.findAllDirtyAnalyses();
        if(dirtyAnalyses != null && dirtyAnalyses.size() > 0) {
            LOGGER.info("Found " + dirtyAnalyses.size() + " dirty analyses. Cleaning... ");
            for (Analysis currentAnalysis : dirtyAnalyses) {
                //Reset AnalysisState so it is picked up by ProcessingThread on next run
                currentAnalysis.setAnalysisState(AnalysisState.CREATED);
            }
            analysisService.saveAll(dirtyAnalyses);
        } else {
            LOGGER.info("No dirty analyses found.");
        }
    }

}


我在run方法的第一行和第二行放置了一个断点。如果我使用ScheduledFuture.get(),第一行被调用,然后调用JPA存储库方法,但它永远不会返回.它不会在控制台中生成查询.
如果我使用了ScheduledFuture.isDone(),run方法根本不会被调用.
我进一步挖掘了这个问题,这是我发现它停止工作的地方:
1.我使用scheduledFuture.get()来等待任务完成
1.调用AnalysisCleaningThread的run()方法中的第一行代码,该代码应调用服务以检索分析列表
1.调用CglibAopProxy来拦截该方法
1.调用ReflectiveMethodInvocation -> TransactionInterceptor -> TransactionAbstract Support-> DefaultListableBeanFactory -> AbstractBeanFactory来按类型搜索和匹配PlatformTransactionManager bean

  1. getSingleton用beanName**“main”调用,在行187**synchronized(this.singletonObjects),应用程序暂停,不再继续
    从我的Angular 来看,似乎this.singletonObjects目前正在使用,所以线程无法继续以某种方式.
nukf8bse

nukf8bse1#

所以我做了很多研究,因为这个问题发生,终于找到了解决我罕见的情况。
我首先注意到的是,没有future.get(),AnalysisCleaningThread运行没有任何问题,但run方法花了大约2秒的时间执行第一行,所以我认为在最终进行数据库调用之前,后台一定发生了一些事情。
我在最初的问题编辑中通过调试发现,应用程序在DefaultSingletonBeanRegistry.getSingleton方法的同步块synchronized(this.singletonObjects)处停止在第93行,所以一定有什么东西持有那个锁对象。实际上,当调用DefaultSingletonBeanRegistry.getSingleton的迭代方法将“main”作为参数“beanName”传递到getSingleton时,它就停止在那一行。

  • 顺便说一句,调用该方法(或更好的方法链)是为了获得PlatformTransactionManager bean的示例,以进行该服务(数据库)调用。

我当时的第一个想法是,这一定是一个僵局。

最后的思考

据我所知,bean在其生命周期内还没有最终就绪(仍然在它的@PostConstruct init()方法中)。当spring试图获取平台事务管理器的示例以进行数据库查询时,应用程序死锁。它实际上死锁是因为在迭代所有bean名称以查找PlatformTansactionManager时,它还试图解析当前正在等待的“main”bean,因为它的@PostConstruct方法中有future.get()。因此,它无法获得示例,并且永远等待锁被释放。

溶液

因为Main.class是我的入口点,我不想把代码放在另一个类中,所以我开始寻找一个钩子,它在应用程序完全启动后启动任务。
我偶然发现了@EventListener,在我的例子中,它监听ApplicationReadyEvent.class,瞧,它工作了。下面是我的代码解决方案。

@SpringBootApplication
@ComponentScan("de. ... .analysis")
@EntityScan("de. ... .persistence")
@EnableJpaRepositories("de. ... .persistence.repositories")
@EnableAutoConfiguration
@PropertySources(value = {@PropertySource("classpath:application.properties")})
public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static SpringApplication application = new SpringApplication(Main.class);

    private TaskScheduler taskScheduler;

    private AnalysisProcessingThread processingThread;

    private AnalysisCleaningThread cleaningThread;

    @Inject
    public void setProcessingThread(AnalysisProcessingThread processingThread) {
        this.processingThread = processingThread;
    }

    @Inject
    public void setCleaningThread(AnalysisCleaningThread cleaningThread) {
        this.cleaningThread = cleaningThread;
    }

    @Inject
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public static void main(String[] args)
            throws Exception {

        try {

            //Do some setup

            application.run(args);

        } catch (Exception e) {

            LOGGER.error(e.getMessage(), e);
        }
    }

    @PostConstruct
    public void init() throws Exception {

        //Do some other setup

    }

    @EventListener(ApplicationReadyEvent.class)
    public void startAndScheduleTasks() {
        ScheduledFuture scheduledFuture = null;
        LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********");
        Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant());
        scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds);
        try {
            scheduledFuture.get();
        } catch (InterruptedException | ExecutionException e) {
            LOGGER.error("********** Cleaning Thread did not finish as expected! Stopping thread. Dirty analyses may still remain in database **********", e);
            scheduledFuture.cancel(true);
        }
  }
}

字符串

摘要

在极少数情况下,从@PostConstruct方法执行spring data repository调用可能会发生死锁,如果在spring可以获取PlatformTransactionManager bean以执行spring data repository查询之前,使用@PostConstruct标注的方法没有结束。无论是无限循环还是future.get()方法,都不会发生死锁。它迭代所有注册的beanNames,最后调用DefaultSingletonBeanRegistry.getSingleton以找到PlatformTransactionManager bean,用当前在@PostConstruct方法中的bean名称调用getSingleton。如果它在此之前找到PlatformTransactionManager,那么它不会发生。

相关问题