Друкарня від WE.UA

Як насправді працює @Async у Spring і коли його використання створює більше проблем, ніж вирішує

Зміст

В теорії, анотація @Async у Spring виглядає як магічне рішення для асинхронного виконання методів. Додали анотацію, і метод виконується в окремому потоці, Але за цією простотою ховаються нюанси, які можуть призвести до серйозних проблем.

Як це працює за кулісами

Коли ви анотуєте метод за допомогою @Async, Spring створює проксі-об'єкт навколо вашого компонента. При виклику асинхронного методу, Spring перехоплює виклик і делегує його виконання в окремий пул потоків (TaskExecutor).

@Service
public class EmailService {
    @Async
    public CompletableFuture<Boolean> sendEmail(String to, String content) {
        // Довга операція
        return CompletableFuture.completedFuture(true);
    }
}

Здавалося б, просто і зручно. Але тут починаються проблеми :)

Проблема #1. Неможливість перехоплення виключень

Оскільки метод виконується асинхронно, виключення, які виникають у ньому, не будуть автоматично перехоплені блоками try-catch навколо виклику методу.

try {
    emailService.sendEmail("user@example.com", "Hello"); // Помилка не буде перехоплена!
} catch (Exception e) {
    // Цей код ніколи не виконається
}

Єдиним способом обробити такі виключення є використання спеціального AsyncUncaughtExceptionHandler або обробка помилок через CompletableFuture.

Проблема #2. Втрата транзакційного контексту

Один з найнеприємніших сюрпризів: @Async і @Transactional погано поєднуються. Коли метод виконується в іншому потоці, він втрачає транзакційний контекст викликаючого методу.

@Service
public class UserService {
    @Transactional
    public void registerUser(User user) {
        repository.save(user);
        notificationService.sendWelcomeEmail(user); // Асинхронний метод
        // Якщо sendWelcomeEmail() викине виключення, user все одно збережеться
    }
}

Проблема #3. Непередбачувана поведінка при self-invocation

Виклик асинхронного методу з того ж класу (this.asyncMethod()) не працюватиме, оскільки Spring використовує проксі для перехоплення викликів, а при self-invocation проксі не задіюється.

@Service
public class AnalyticsService {
    public void process(Data data) {
        // Це НЕ буде асинхронним
        this.processAsync(data);
    }
    
    @Async
    public void processAsync(Data data) {
        // ...
    }
}

Проблема #4. Важко тестувати

Асинхронні методи надзвичайно складно тестувати в юніт-тестах. Вам потрібно або мокати TaskExecutor, або використовувати складні конструкції з CountDownLatch для очікування завершення виконання. Або додаткові ліби по типу Awaitility.

Проблема #5. Дефолтний TaskExecutor

Якщо ви не налаштували власний TaskExecutor, Spring використовуватиме SimpleAsyncTaskExecutor, який створює новий потік для кожного виклику. При високому навантаженні це призведе до вичерпання ресурсів сервера.

Крім того, при використанні асинхронних методів часто втрачається контекст логування (MDC), що ускладнює відстеження запитів у логах.

Створення власного TaskExecutor

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // Основні налаштування пулу потоків
        executor.setCorePoolSize(5);          // Базова кількість потоків
        executor.setMaxPoolSize(10);          // Максимальна кількість потоків
        executor.setQueueCapacity(25);        // Розмір черги завдань
        
        // Що робити, коли черга заповнена
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        // Важливо для відстеження в логах!
        executor.setThreadNamePrefix("AsyncTask-");
        
        // Копіювання MDC контексту
        executor.setTaskDecorator(new MdcTaskDecorator());
        
        // Не забудьте це для коректної ініціалізації
        executor.initialize();
        
        return executor;
    }
    
    // Обробка виключень у асинхронних методах
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

Вирішення проблеми втрати MDC контексту

MDC (Mapped Diagnostic Context) використовується для збереження контексту запиту в логах. При асинхронному виконанні цей контекст втрачається, оскільки потоки не успадковують MDC автоматично.

Ось імплементація декоратора, який зберігає MDC контекст:

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        // Копіюємо поточний MDC контекст
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        
        return () -> {
            try {
                // Відновлюємо MDC контекст у новому потоці
                if (contextMap != null) {
                    MDC.setContextMap(contextMap);
                }
                // Виконуємо нашу таску
                runnable.run();
            } finally {
                // Очищаємо контекст після виконання
                MDC.clear();
            }
        };
    }
}

Обробка асинхронних виключень

public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(CustomAsyncExceptionHandler.class);
    
    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        logger.error("Async exception in method: {}.{} with params: {}",
            method.getDeclaringClass().getSimpleName(),
            method.getName(),
            Arrays.toString(params),
            ex);
            
        // Можна також надіслати повідомлення в систему моніторингу
        // або додати запис у базу даних помилок
    }
}

Використання налаштованого TaskExecutor

@Service
public class EmailService {
    
    @Async("taskExecutor")  // Явно вказуємо ім'я нашого пулу потоків
    public CompletableFuture<Boolean> sendEmail(String to, String content) {
        // MDC контекст збережеться завдяки декоратору
        logger.info("Sending email to: {}", to); // traceId/requestId збережеться
        
        // Довга операція...
        
        return CompletableFuture.completedFuture(true);
    }
}

Приклад проблеми з логами без збереження MDC

Без збереження MDC:

[2023-03-01 10:15:23.456] [requestId=abc-123] [Thread-1] INFO - Отримано запит надіслати email
[2023-03-01 10:15:23.458] [Thread-2] INFO - Sending email to user@example.com

Зі збереженням MDC:

[2023-03-01 10:15:23.456] [requestId=abc-123] [Thread-1] INFO - Отримано запит надіслати email
[2023-03-01 10:15:23.458] [requestId=abc-123] [AsyncTask-2] INFO - Sending email to user@example.com

Статті про вітчизняний бізнес та цікавих людей:

  • Як побудована програма Meest China Academy

    Курс про товарний бізнес охоплює різні етапи роботи: від пошуку ідеї до перевірки товару, логістики та масштабування. Програма Meest China Academy містить 17 модулів, які послідовно розкривають ці теми без зведення всього навчання до однієї універсальної поради.

    Теми цього довгочиту:

    Meest
  • Які технології використані в Айфон 17

    Айфон 17 поєднує OLED-дисплей із частотою до 120 Гц, чип A19, дві камери Fusion 48 Мп і швидке заряджання через USB-C. У COMFY можна купити Айфон 17 з накопичувачем на 256 або 512 ГБ, вибравши конфігурацію відповідно до обсягу фото, відео та застосунків

    Теми цього довгочиту:

    Iphone 17
  • Як вибрати вживаний iPhone: коротке керівництво для розумної покупки

    Вторинний ринок смартфонів Apple активно зростає, і питання, як вибрати вживаний iPhone, стає актуальним для дедалі більшої кількості користувачів. Правильний підхід дозволяє отримати бу iPhone з актуальною iOS і доброю камерою за помітно нижчу ціну, ніж у нових пристроїв.

    Теми цього довгочиту:

    Iphone
  • Як подолати жіночу безплідність за допомогою донорських яйцеклітин

    Діагноз "безпліддя" рідко звучить як вирок одразу. Для багатьох жінок саме в цей момент вперше звучить словосполучення "донорські яйцеклітини" — і це часто не кінець шляху до материнства, а якраз новий, реальний шанс.

    Теми цього довгочиту:

    Донорство
  • Як заспокоїти зубний біль і чому самолікування може призвести до операції?

    Раптовий біль у зубі може призвести до порушення сну та ускладненого вживання їжі. Люди часто приймають таблетки, роблять полоскання або лікуються народними засобами. В таких випадках не варто відкладати візит до стоматолога, адже можуть з’явитись ускладнення.

    Теми цього довгочиту:

    Стоматологія
Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Oleksandr Klymenko
Oleksandr Klymenko@overpathz

Java Software Engineer

40Довгочити
9.4KПерегляди
104Підписники
На Друкарні з 19 квітня 2023

Більше від автора

  • Java Memory Model ↔ PostgreSQL MVCC: один і той самий фундамент конкурентності

    Java Memory Model та PostgreSQL MVCC/Isolation Levels — це дві відповіді на одне й те саме фундаментальне питання: за яких умов зміна, зроблена одним учасником (потоком / транзакцією), стає видимою для іншого?

    Теми цього довгочиту:

    Java
  • Як виконується SQL-запит

    Сьогодні розберемо, що відбувається з вашим SELECT * FROM users WHERE age > 21 перед тим, як БД поверне вам дані

    Теми цього довгочиту:

    Computer Science

Це також може зацікавити:

  • Користь університетів для ІТ спеціалістів

    Проходячи будь-які курси або школу ви засвоюєте певні уміння та навички, тренуючи які - отримуєте практичні знання які конвертуються у досвід.

    Теми цього довгочиту:

    It
  • Tqdm — просте й швидке створення прогрес барів у консолі

    Всіх вітаю! Чи цікавилися ви коли небудь як створювати прогрес бари прямо у консолі? Якщо ні, то дана стаття саме для вас! У цій невеличкій статті ми дізнаємося, яким чином можна сторювати прогрес бари за допомогою бібліотеки tqdm у Python.

    Теми цього довгочиту:

    Програмування
  • java training near me

    In today’s competitive IT world, mastering Java and Full Stack Development opens doors to a wide range of career opportunities. At V Cube Software Solutions, we offer the Best Full Stack Developer Course in KPHB, designed for aspiring developers who want to

    Теми цього довгочиту:

    Java

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Це також може зацікавити:

  • Користь університетів для ІТ спеціалістів

    Проходячи будь-які курси або школу ви засвоюєте певні уміння та навички, тренуючи які - отримуєте практичні знання які конвертуються у досвід.

    Теми цього довгочиту:

    It
  • Tqdm — просте й швидке створення прогрес барів у консолі

    Всіх вітаю! Чи цікавилися ви коли небудь як створювати прогрес бари прямо у консолі? Якщо ні, то дана стаття саме для вас! У цій невеличкій статті ми дізнаємося, яким чином можна сторювати прогрес бари за допомогою бібліотеки tqdm у Python.

    Теми цього довгочиту:

    Програмування
  • java training near me

    In today’s competitive IT world, mastering Java and Full Stack Development opens doors to a wide range of career opportunities. At V Cube Software Solutions, we offer the Best Full Stack Developer Course in KPHB, designed for aspiring developers who want to

    Теми цього довгочиту:

    Java