@Scheduled + @Async (в Spring Boot)

от автора

Недавно отвечал на вопрос почему аннотации @Scheduled и @Async иногда используют вместе, данный вопрос попался человеку на собеседовании.

Многие начинающие разработчики на java не до конца понимают в каких потоках происходит выполнение программы в таком случае.

В данном материале постараюсь объяснить зачем аннотации @Scheduled и @Async ставят вместе, какая проблема при этом решается, в каких потоках происходит работа программы и как делать правильно.

Подписывайтесь на мой блог в телеграм, где я раньше всего публикую материалы.

Проблема

Допустим вам необходимо выполнять какое-то действие раз в минуту, например ходить в базу и смотреть есть ли там записи по которым не выполнилась отправка во внешнюю систему и если такие есть то выполнить отправку повторно (доотправка).

И еще одно действие, которое надо выполнять раз в день, например сформировать аналитический отчет, и формироваться он будет очень долго, целых 5 минут.

Действия которые нужно выполнять по времени я буду называть джобами (от слова job).

Это вполне реальная ситуация на коммерческом проекте.

И мы начали замечать, что во время формирования отчета (а мы помним что это длится 5 минут), первый джоб доотправки не выполняется, почему так присходит?

Представим эту ситуацию в коде.

Пометим два метода аннотацией @Scheduled:

  1. первый будет выполняться раз в секунду и выводить в лог текущее время и имя потока;

  2. второй будет запускаться раз в 5 секунд и выводить в лог текущее время, имя потока и вдобавок зависать на 3 секунды, имитируя тяжеловесную операцию.

@Scheduled(cron = "*/1 * * * * *")   public void everySecond() {       log.info("EverySecond. Время: {}. Поток: {}",      LocalTime.now(), Thread.currentThread().getName());   }    @Scheduled(cron = "*/5 * * * * *")   public void everyFiveSeconds() throws InterruptedException {       log.info("EveryFiveSeconds. Время: {}. Поток: {}",      LocalTime.now(), Thread.currentThread().getName());       Thread.sleep(3000); } 

Вывод в консоль:

EverySecond. Время: 10:56:12.003384. Поток: scheduling-1 EverySecond. Время: 10:56:13.002846. Поток: scheduling-1 EverySecond. Время: 10:56:14.002538. Поток: scheduling-1 EverySecond. Время: 10:56:15.003404. Поток: scheduling-1 EveryFiveSeconds. Время: 10:56:15.004427. Поток: scheduling-1 EverySecond. Время: 10:56:18.005196. Поток: scheduling-1 EverySecond. Время: 10:56:19.002534. Поток: scheduling-1 EveryFiveSeconds. Время: 10:56:20.002549. Поток: scheduling-1 EverySecond. Время: 10:56:23.003966. Поток: scheduling-1 EverySecond. Время: 10:56:24.002588. Поток: scheduling-1 EverySecond. Время: 10:56:25.002563. Поток: scheduling-1 EveryFiveSeconds. Время: 10:56:25.003424. Поток: scheduling-1

Разберем что мы увидели в логе.

Оба метода выполняются в одном и том же потоке.

При этом пока 3 секунды выполняется тяжеловсный джоб, второй джоб не выполняется вовсе, потому что единственный поток занят.

На коммерческом проекте это может стать проблемой, и у вас уйдет много времени чтобы разобраться почему ваш @Scheduled метод не отрабатывает в заданное время.

Решение проблемы

Решение — передать выполнение джобов выделенному пулу потоков.

И есть несколько вариантов передачи выполнения пулу:

  1. настроить аннотацию @Scheduled чтобы метод помеченный данной аннотацией всегда выполнялся в отдельном потоке;

  2. явно передавать выполнение кода в отдельный поток, например с помощью аннотации @Async;

Рассмотрим оба варианта.

Настроить пул потоков для аннотации @Scheduled

Если обратиться к javadoc аннотации @EnableScheduling то найдем пример конфигурации:

@Configuration   public class SchedulerConfig implements SchedulingConfigurer {          @Override       public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {           taskRegistrar.setScheduler(taskExecutor());       }          @Bean(destroyMethod = "shutdown")       public Executor taskExecutor() {           return Executors.newScheduledThreadPool(100);       }   } 

Добавим её в наше приложение и перезапустим.

Вывод в лог:

EverySecond. Время: 11:41:00.003843. Поток: pool-2-thread-2 EverySecond. Время: 11:41:01.001589. Поток: pool-2-thread-3 EverySecond. Время: 11:41:02.000566. Поток: pool-2-thread-2 EverySecond. Время: 11:41:03.002542. Поток: pool-2-thread-4 EverySecond. Время: 11:41:04.002607. Поток: pool-2-thread-3 EveryFiveSeconds. Время: 11:41:05.005270. Поток: pool-2-thread-5 EverySecond. Время: 11:41:05.005244. Поток: pool-2-thread-2 EverySecond. Время: 11:41:06.003887. Поток: pool-2-thread-4 EverySecond. Время: 11:41:07.003836. Поток: pool-2-thread-7 EverySecond. Время: 11:41:08.001952. Поток: pool-2-thread-1 EverySecond. Время: 11:41:09.005323. Поток: pool-2-thread-8 EverySecond. Время: 11:41:10.001659. Поток: pool-2-thread-9 EveryFiveSeconds. Время: 11:41:10.001661. Поток: pool-2-thread-3

Видим, что запуск каждого джоба выполняется в отдельном потоке, при этом тяжеловесный джоб не мешает запуску легковесного.

Проблема решена.

Решение через аннотацию @Async

Поставим аннотацию @Async над @Scheduled методами:

@Async   @Scheduled(cron = "*/1 * * * * *")   public void everySecond() {       log.info("EverySecond. Время: {}. Поток: {}",      LocalTime.now(), Thread.currentThread().getName());   }      @Async   @Scheduled(cron = "*/5 * * * * *")   public void everyFiveSeconds() throws InterruptedException {       log.info("EveryFiveSeconds. Время: {}. Поток: {}",      LocalTime.now(), Thread.currentThread().getName());          Thread.sleep(3000);   } 

Вывод в лог будет аналогичен предыдущему примеру, т.е. джобы будут выполняться в отдельных потоках и не мешать друг другу.

На что стоит обратить внимание

При использовании аннотации @Async по умолчанию Spring создает Executor, у которого нет предела по количеству создаваемых потоков.

Тем самым, если ваш джоб будет запускаться чаще чем завершаться, то возникнет утечка памяти из-за постоянно создаваемых потоков.

Решить проблему можно с помощью настройки Executor для @Async, пример настройки можно посмотреть в javadoc аннотации @EnableAsync.

Правильная настройка Executor’ов это тема для отдельной статьи.

Какой вариант выбрать

Если у вас один джоб на всё приложение и он выполняется быстрее чем запускается, то можно ничего не делать и пользоваться аннотацией @Scheduled в том виде как она работает из коробки.

Если у вас несколько джобов то лучше всегда настраивать явный планировщик задач, и для удобства пользоваться аннотацией @Async.

Стоит заметить, что при использовании @Async лучше всегда указывать Executor явно, пример:

@Bean   public Executor jobExecutor() {       return Executors.newCachedThreadPool();   }   
@Async("jobExecutor")   @Scheduled(cron = "*/1 * * * * *")   public void everySecond() {       ... } 

тогда вы гарантируете, что потоки Executor’а не будут заняты другими задачами кроме ваших джобов.


ссылка на оригинал статьи https://habr.com/ru/articles/771112/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *