Tarefas Agendadas e Processamento Assíncrono
Tarefas agendadas e processamento assíncrono são as chaves para aumentar a eficiência do backend—agende o que precisa ser agendado e execute em paralelo o que pode ser executado em paralelo, para que os usuários não precisem esperar desnecessariamente.
1. O Que Você Vai Aprender
@EnableSchedulinge Expressões Cron / FixedRate / FixedDelay- Execução assíncrona com
@EnableAsynce@Async - Configuração de Pool de Threads Personalizado
ThreadPoolTaskExecutor - Tratamento de Exceções Assíncronas
AsyncUncaughtExceptionHandler - Alice implementou uma tarefa agendada para cancelar automaticamente pedidos que não foram pagos em 30 minutos
2. Uma História Real de um Desenvolvedor de Negócios
(1) Dor: Acúmulo de Pedidos com Tempo Esgotado
Alice descobriu um grande acúmulo de pedidos não pagos no OrderFlow—os usuários faziam pedidos mas não pagavam, o que bloqueava o estoque e impedia outros usuários de comprar. Charlie relatou que havia aproximadamente 2.000 pedidos com tempo esgotado por dia, bloqueando coletivamente um estoque no valor de 50.000 USD. Anteriormente, os pedidos eram cancelados manualmente verificando o banco de dados a cada 30 minutos, o que era ineficiente e propenso a omissões.
(2) Solução com @Scheduled
O Spring Boot inclui tarefas agendadas integradas que podem ser implementadas com uma única anotação:
@Scheduled(fixedRate = 5 * 60 * 1000) // A cada 5 minutos
public void cancelExpiredOrders() {
List<Order> expired = orderRepository.findByStatusAndCreatedAtBefore(
"PENDING", Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> cancelOrder(order));
log.info("Cancelled {} expired orders", expired.size());
}
(3) Resultado
Depois que Alice implementou a tarefa agendada, os pedidos expirados são limpos automaticamente a cada 5 minutos, o estoque é liberado automaticamente, e o acúmulo de pedidos expirados caiu de 2.000 por dia para 0. Ao mesmo tempo, ao usar @Async para mudar os e-mails de notificação para entrega assíncrona, o tempo de resposta da API de realização de pedidos caiu de 800 ms para 50 ms.
3. Tarefas Agendadas com @Scheduled
(1) Três Métodos de Agendamento
| Método | Parâmetro | Significado | Exemplo |
|---|---|---|---|
| FixedRate | fixedRate |
Intervalo fixo (contando a partir da última vez) | Executar a cada 5 minutos |
| FixedDelay | fixedDelay |
Atraso fixo (a partir do final da última execução) | Esperar 5 minutos após o final da última execução |
| Cron | cron |
Expressão Cron | Executa às 2:00 da manhã todos os dias |
gantt
title Temporização FixedRate vs FixedDelay
dateFormat X
axisFormat %s
section FixedRate
Tarefa A (5min) :a1, 0, 5
Tarefa B (5min) :a2, 5, 8
Tarefa C (5min) :a3, 10, 14
section FixedDelay
Tarefa A (5min) :b1, 0, 5
espera 3min :b2, 5, 8
Tarefa B (5min) :b3, 8, 13
espera 3min :b4, 13, 16
| Dimensão | FixedRate | FixedDelay | Cron |
|---|---|---|---|
| Intervalo de Execução | Fixo (independentemente da duração anterior) | Fixo (após o término da anterior) | Horário Exato |
| Sobreposição de Tarefas | Pode Sobrepor | Não Sobreponha | Pode Sobrepor |
| Casos de Uso | Polling | Processamento de Dados | Processamento em Lote Agendado |
(1) ▶ Exemplo: Tarefa Agendada com FixedRate
@Component
@EnableScheduling
public class OrderScheduleTask {
private final OrderService orderService;
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRate = 5 * 60 * 1000) // A cada 5 minutos
public void cancelExpiredOrders() {
log.info("Iniciando limpeza de pedidos expirados...");
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(30));
log.info("Cancelados {} pedidos expirados", cancelled);
}
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Expressões Cron para Tarefas Agendadas
@Component
public class DailyReportTask {
@Scheduled(cron = "0 0 2 * * ?") // Todos os dias às 2:00 da manhã
public void generateDailyReport() {
// Gerar relatório diário de estatísticas de pedidos
}
@Scheduled(cron = "0 0 0 1 * ?") // Primeiro dia de cada mês à meia-noite
public void generateMonthlyReport() {
// Gerar relatório mensal
}
}
Saída:
// Execução bem-sucedida
| Campo Cron | Significado | Valores Válidos |
|---|---|---|
| Segundos | 0-59 | |
| Min | Minutos | 0-59 |
| Hora | Horas | 0-23 |
| Dia | Dia do mês | 1-31 |
| Mês | 1-12 | |
| Dia da Semana | Dia da semana | 0-7 (0/7 = Dom) |
| Expressões Cron Comuns | Significado |
|---|---|
0 0 2 * * ? |
Todos os dias às 2 da manhã |
0 */5 * * * ? |
A cada 5 minutos |
0 0 0 1 * ? |
Meia-noite no dia 1 de cada mês |
0 0 9-17 * * MON-FRI |
Dias úteis, 9:00-17:00 (em ponto) |
4. Processamento Assíncrono com @Async
(1) Síncrono vs. Assíncrono
(1) ▶ Exemplo: Envio de Notificações em Paralelo (Bloqueante)
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
emailService.sendConfirmation(order); // Bloqueia por 500ms
smsService.sendNotification(order); // Bloqueia por 200ms
return order; // Total: 700ms desperdiçados
}
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Envio de Notificações Assincronamente (Não-Bloqueante)
@Service
@EnableAsync
public class NotificationService {
@Async
public void sendOrderConfirmation(Order order) {
// Isso executa em uma thread separada
// Não bloqueia o chamador
emailService.sendConfirmation(order);
log.info("E-mail de confirmação enviado para o pedido {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
smsService.sendNotification(order);
return CompletableFuture.completedFuture("SMS enviado");
}
}
// No OrderService
@Service
public class OrderService {
private final NotificationService notificationService;
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
notificationService.sendOrderConfirmation(order); // Não-bloqueante
return order; // Retorna imediatamente
}
}
Saída:
// Execução bem-sucedida
| Dimensão | Síncrono | Assíncrono |
|---|---|---|
| Método de Chamada | Bloquear e esperar o resultado | Retornar imediatamente; executar em segundo plano |
| Tempo de Resposta | Tempo Total de Todas as Operações | Apenas Tempo das Operações de Negócio Principal |
| Tratamento de Erros | Tratamento direto try-catch | AsyncUncaughtExceptionHandler |
| Casos de Uso | Operações Que Requerem Espera pelo Resultado | Notificações, Logs, Operações Não Críticas |
5. Pool de Threads Personalizado
(1) Problemas com o Pool de Threads Padrão
O Spring Boot usa SimpleAsyncTaskExecutor por padrão, que cria uma nova thread para cada tarefa e não limita o número de threads, o que pode resultar em um OutOfMemoryError.
(1) ▶ Exemplo: ThreadPoolTaskExecutor Personalizado
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("orderflow-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Exceção assíncrona no método {}: {}", method.getName(), ex.getMessage(), ex);
};
}
}
Saída:
// Execução bem-sucedida
| Parâmetro | Significado | Valor Recomendado |
|---|---|---|
corePoolSize |
Número de Threads Principais (Residentes) | Número de Núcleos de CPU |
maxPoolSize |
Número máximo de threads | Número de núcleos de CPU x 2 |
queueCapacity |
Capacidade da Fila | 100-1.000 |
threadNamePrefix |
Prefixo do Nome da Thread | NomeDoProjeto-async- |
rejectedExecutionHandler |
Política de Rejeição | CallerRunsPolicy |
| Estratégia de Rejeição | Comportamento | Cenários Aplicáveis |
|---|---|---|
AbortPolicy |
Lança RejectedExecutionException | Padrão, falha rápida |
CallerRunsPolicy |
Executado pela thread do chamador | Degradado mas não descartado |
DiscardPolicy |
Descarte Silencioso | Perda Tolerável |
DiscardOldestPolicy |
Descartar a tarefa mais antiga | Priorizar desempenho em tempo real |
6. Tratamento de Exceções Assíncronas
(1) ▶ Exemplo: AsyncUncaughtExceptionHandler
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("Método assíncrono [{}] lançou exceção: {}",
method.getName(), throwable.getMessage(), throwable);
// Também poderia publicar no sistema de monitoramento
};
}
}
Saída:
// Execução bem-sucedida
@Async não são propagadas para o chamador e devem ser tratadas usando AsyncUncaughtExceptionHandler. Exceções lançadas por métodos que retornam CompletableFuture podem ser tratadas usando future.exceptionally().
7. Exemplo Abrangente: Cancelamento por Tempo Esgotado + Notificação Assíncrona do OrderFlow
// OrderScheduleTask.java
package com.orderflow.schedule;
import com.orderflow.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class OrderScheduleTask {
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
private final OrderService orderService;
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
public void cancelExpiredOrders() {
int timeoutMinutes = 30;
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(timeoutMinutes));
if (cancelled > 0) {
log.info("Cancelados {} pedidos expirados por {} minutos", cancelled, timeoutMinutes);
}
}
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
log.info("Gerando relatório diário de pedidos...");
orderService.generateDailyReport();
}
}
// NotificationService.java
package com.orderflow.service;
import com.orderflow.model.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
@Async
public void sendOrderConfirmation(Order order) {
// Simular envio de e-mail
log.info("Enviando e-mail de confirmação para o pedido {}", order.getId());
}
@Async
public void sendCancellationNotice(Order order) {
log.info("Enviando aviso de cancelamento para o pedido {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
log.info("Enviando SMS para o pedido {}", order.getId());
return CompletableFuture.completedFuture("SMS enviado para o pedido " + order.getId());
}
}
// AsyncConfig.java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("orderflow-async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.setWaitForTasksToCompleteOnShutdown(true);
exec.setAwaitTerminationSeconds(60);
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Erro assíncrono em {}: {}", method.getName(), ex.getMessage(), ex);
}
}
❓ Perguntas Frequentes
TaskScheduler, ou delegue operações demoradas ao pool de threads usando @Async.fixedRate e fixedDelay para tarefas de longa duração?fixedRate conta a partir do horário de início da tarefa anterior; se uma tarefa demorar mais que o intervalo, ela será executada consecutivamente ou até mesmo se sobrepor a outras tarefas. fixedDelay conta a partir do horário de término da tarefa anterior, garantindo um intervalo fixo entre tarefas para que não se sobreponham.ScheduledTaskRegistrar para registrar tarefas programaticamente, ou usar o Centro de Configuração para atualizá-las dinamicamente. Uma solução simples: fixedRateString = "${interval}"—controle via configuração externa.CompletableFuture e um que retorna void?void é do tipo "dispare e esqueça"; o chamador não se importa com o resultado, e as exceções são tratadas pelo AsyncUncaughtExceptionHandler. Métodos CompletableFuture permitem tratamento encadeado de resultados e exceções, dando ao chamador mais controle.📖 Resumo
@ScheduledTrês métodos de agendamento: fixedRate (intervalo fixo), fixedDelay (atraso fixo), cron (horário exato)@AsyncEvita que operações demoradas bloqueiem o chamador; adequado para operações não críticas como notificações e logging- O pool de threads padrão não limita o número de threads; em ambiente de produção, você deve configurar seu próprio
ThreadPoolTaskExecutor AsyncUncaughtExceptionHandlerTratamento de Exceções em Métodos Assíncronos void- Ao implantar múltiplas instâncias de uma tarefa agendada, é necessário um lock distribuído (como ShedLock) para evitar execuções duplicadas.
@AsyncChamadas entre métodos da mesma classe não funcionam; é necessário decompor a classe ou injetar um proxy.
📝 Exercícios
-
Problema Básico (Dificuldade ⭐): Implemente uma tarefa agendada para o OrderFlow para cancelar automaticamente pedidos com tempo esgotado. A cada 5 minutos, verifique e cancele quaisquer pedidos PENDING que não foram pagos por 30 minutos.
-
Exercício Avançado (Dificuldade: ⭐⭐): Modifique o e-mail de confirmação do pedido e as notificações SMS para serem enviadas assincronamente usando @Async, personalize o pool de threads (core=5, max=20) e verifique se o tempo de resposta da API de realização de pedidos foi significativamente reduzido.
-
Desafio (Dificuldade: ⭐⭐⭐): Integre o ShedLock para implementar um lock distribuído para tarefas agendadas, garantindo que quando o OrderFlow for implantado em múltiplas instâncias, apenas uma instância execute uma tarefa agendada em qualquer momento. Considere a granularidade do lock e o design de timeout.



