Как в Spring реализовать фоновое выполнение задач вне основного потока
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ
В Spring Framework есть несколько способов запустить задачу в фоновом потоке и не блокировать основной поток обработки запроса. Каждый способ подходит для разных сценариев.
Главное правило
Если операция занимает > 100 мс, не держите пользователя ждать. Запустите её асинхронно и вернитесь к пользователю с подтверждением ("в очереди").
Способ 1: @Async (самый простой)
Шаг 1: Включить поддержку асинхронности
@SpringBootApplication
@EnableAsync // это главное!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
Шаг 2: Отметить метод как асинхронный
@Service
public class EmailService {
// Этот метод выполняется в отдельном потоке
@Async
public void sendWelcomeEmail(User user) {
// Долгая операция (отправка письма)
mailSender.send(createMessage(user));
logger.info("Email sent to " + user.getEmail());
}
@Async
public CompletableFuture<String> sendAndReturnStatus(User user) {
try {
mailSender.send(createMessage(user));
return CompletableFuture.completedFuture("Sent successfully");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
// Использование
@RestController
public class UserController {
@Autowired
private EmailService emailService;
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody UserDTO dto) {
User user = userService.createUser(dto);
// Отправляем письмо в фоне (сразу вернемся ответ)
emailService.sendWelcomeEmail(user);
return ResponseEntity.ok(user); // не ждем письма
}
}
Как это работает:
Приходит запрос POST /users
↓
Создаем пользователя в БД (10 мс)
↓
@Async запускает отправку письма в новом потоке
↓
Отвечаем пользователю: "OK" (сразу)
↓
(в фоне: отправляем письмо, может занять 2 сек)
Проблемы default @Async:
- По умолчанию использует SimpleAsyncTaskExecutor (создает новый поток на каждый вызов)
- Медленно при большом количестве задач
Способ 2: Настройка ThreadPoolTaskExecutor
Проблема: default executor неэффективен. Решение — использовать пул потоков.
@Configuration
@EnableAsync
public class AsyncConfiguration {
// Конфигурируем пул потоков для @Async
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Основной размер пула (всегда готовые потоки)
executor.setCorePoolSize(5);
// Максимум потоков (когда очередь переполняется)
executor.setMaxPoolSize(20);
// Очередь для задач (если нет свободных потоков)
executor.setQueueCapacity(100);
// Время жизни потока (если работает < этого времени, удаляем)
executor.setKeepAliveSeconds(60);
// Имя потока (для логов и отладки)
executor.setThreadNamePrefix("async-task-");
// Что делать, если очередь переполняется
executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// Использование
@Service
public class EmailService {
@Async("taskExecutor") // используем наш configured executor
public void sendEmail(User user) {
mailSender.send(createMessage(user));
}
}
Способ 3: CompletableFuture (более гибко)
Для операций, результат которых нужно получить позже:
@Service
public class ProcessingService {
@Async
public CompletableFuture<ProcessResult> processLargeData(File file) {
try {
logger.info("Starting processing...");
ProcessResult result = heavyProcessing(file);
logger.info("Finished processing");
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
// Использование
@RestController
public class FileController {
@Autowired
private ProcessingService processingService;
@PostMapping("/upload")
public ResponseEntity<?> uploadFile(@RequestParam MultipartFile file) throws Exception {
// Запускаем обработку в фоне
CompletableFuture<ProcessResult> future = processingService
.processLargeData(file);
// Можем ждать результата или нет
return future
.thenApply(result -> ResponseEntity.ok(result))
.exceptionally(e -> ResponseEntity
.status(500)
.body(Map.of("error", e.getMessage())))
.get(); // блокируем и ждем (или не блокируем)
}
// Или вернуть сразу, не ждать
@PostMapping("/upload-async")
public ResponseEntity<String> uploadFileAsync(@RequestParam MultipartFile file) {
processingService.processLargeData(file)
.thenAccept(result -> logger.info("Done: " + result))
.exceptionally(e -> {
logger.error("Error: " + e.getMessage());
return null;
});
return ResponseEntity.accepted()
.body("File is being processed");
}
}
Способ 4: Scheduled Tasks (периодические задачи)
@Configuration
@EnableScheduling
public class SchedulingConfiguration {
}
@Service
public class ReportService {
// Выполняется каждый день в 2:00 AM
@Scheduled(cron = "0 0 2 * * *")
public void generateDailyReport() {
logger.info("Generating daily report...");
// долгая операция
}
// Выполняется каждые 5 минут
@Scheduled(fixedDelay = 300000) // 5 минут в ms
public void refreshCache() {
logger.info("Refreshing cache...");
}
// Выполняется через 1 минуту после последнего завершения
@Scheduled(fixedRate = 60000) // 1 минута
public void cleanupExpiredSessions() {
logger.info("Cleaning up expired sessions...");
}
}
Способ 5: Queue + Worker (для критичных задач)
Когда нужна надежная обработка (даже если сервер упадет, задача не потеряется):
// 1. Создаем сущность для хранения задачи
@Entity
@Getter
@Setter
public class Task {
@Id
private Long id;
private String type; // "SEND_EMAIL", "GENERATE_REPORT"
private String data; // JSON payload
private TaskStatus status; // PENDING, PROCESSING, COMPLETED, FAILED
private LocalDateTime createdAt;
private int retries;
}
// 2. Repository для работы с задачами
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByStatusOrderByCreatedAt(TaskStatus status);
}
// 3. Service для добавления задачи в очередь
@Service
public class TaskQueue {
@Autowired
private TaskRepository taskRepository;
public void addTask(String type, Object data) {
Task task = new Task();
task.setType(type);
task.setData(JsonUtils.toJson(data));
task.setStatus(TaskStatus.PENDING);
task.setCreatedAt(LocalDateTime.now());
task.setRetries(0);
taskRepository.save(task);
}
}
// 4. Worker для обработки задач
@Service
public class TaskWorker {
@Autowired
private TaskRepository taskRepository;
@Autowired
private EmailService emailService;
@Autowired
private ReportService reportService;
@Scheduled(fixedDelay = 5000) // проверяем каждые 5 секунд
public void processPendingTasks() {
List<Task> tasks = taskRepository.findByStatusOrderByCreatedAt(TaskStatus.PENDING);
for (Task task : tasks) {
try {
task.setStatus(TaskStatus.PROCESSING);
taskRepository.save(task);
// Обрабатываем задачу
switch (task.getType()) {
case "SEND_EMAIL":
SendEmailRequest request = JsonUtils.fromJson(task.getData(), SendEmailRequest.class);
emailService.send(request);
break;
case "GENERATE_REPORT":
reportService.generate();
break;
}
task.setStatus(TaskStatus.COMPLETED);
} catch (Exception e) {
logger.error("Task failed: " + e.getMessage());
task.setRetries(task.getRetries() + 1);
if (task.getRetries() > 3) {
task.setStatus(TaskStatus.FAILED);
} else {
task.setStatus(TaskStatus.PENDING);
}
} finally {
taskRepository.save(task);
}
}
}
}
// 5. Использование
@RestController
public class UserController {
@Autowired
private TaskQueue taskQueue;
@Autowired
private UserService userService;
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody UserDTO dto) {
User user = userService.createUser(dto);
// Добавляем задачу отправки письма в очередь
taskQueue.addTask("SEND_EMAIL", new SendEmailRequest(user.getEmail()));
return ResponseEntity.ok(user); // сразу отвечаем
}
}
Способ 6: Spring Cloud Task / Spring Batch
Для очень сложных задач обработки данных:
@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
@Bean
public Job processUsersJob(JobBuilderFactory jobBuilderFactory,
StepBuilderFactory stepBuilderFactory) {
return jobBuilderFactory.get("processUsersJob")
.start(step1(stepBuilderFactory))
.build();
}
@Bean
public Step step1(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("step1")
.<User, ProcessedUser>chunk(100) // 100 записей за раз
.reader(userReader())
.processor(userProcessor())
.writer(userWriter())
.build();
}
}
Сравнение подходов
| Способ | Сложность | Надежность | Скорость | Когда использовать |
|---|---|---|---|---|
| @Async | Простой | Низкая (потеряется при падении) | Высокая | Некритичные фоновые задачи |
| CompletableFuture | Средняя | Низкая | Высокая | Нужна результат позже |
| Scheduled | Простой | Низкая | Средняя | Периодические задачи |
| Queue + Worker | Сложный | Высокая | Средняя | Критичные задачи (платежи, уведомления) |
| Spring Batch | Очень сложный | Высокая | Низкая | Массовая обработка больших данных |
Best Practices
1. Для отправки писем/уведомлений → @Async с ThreadPoolExecutor
@Service
public class EmailService {
@Async("emailExecutor")
public void sendEmail(String to, String subject, String body) {
// отправляем
}
}
2. Для критичных операций → Queue + Worker + DB
// Сохраняем в БД, worker обрабатывает асинхронно
taskQueue.addTask("PROCESS_PAYMENT", paymentData);
3. Для периодических задач → @Scheduled
@Scheduled(cron = "0 0 * * * *") // каждый час
public void refreshData() { }
4. Логируйте все фоновые операции
logger.info("Task started");
logger.info("Task completed");
logger.error("Task failed", exception);
Вывод
В Spring есть несколько способов запустить задачу в фоне:
- @Async — самый простой для быстрых задач
- CompletableFuture — для операций, нужен результат
- @Scheduled — для периодических задач
- Queue + Worker — для надежной обработки
- Spring Batch — для массовой обработки данных
Выбирайте в зависимости от требований к надежности и сложности задачи.