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


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:

JAVA
@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
100%
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

JAVA
@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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Expressões Cron para Tarefas Agendadas

JAVA
@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:

TEXT
// 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)

JAVA
@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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Envio de Notificações Assincronamente (Não-Bloqueante)

JAVA
@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:

TEXT
// 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

JAVA
@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:

TEXT
// 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

JAVA
@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:

TEXT
// Execução bem-sucedida
📌 Ponto Chave: Exceções lançadas por métodos void do @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

JAVA
// 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

P Os métodos @Scheduled só podem ser executados em uma única thread?
R Por padrão, sim. Múltiplos métodos @Scheduled compartilham um agendador de thread única, então tarefas de longa duração bloquearão outras tarefas. Solução: Configure o pool de threads do TaskScheduler, ou delegue operações demoradas ao pool de threads usando @Async.
P O @Async funciona quando chamado dentro da mesma classe?
R Não. Assim como @Transactional, @Async depende de proxies AOP, e chamadas entre métodos da mesma classe contornam o proxy. Soluções: 1) Divida em serviços diferentes; 2) Injete seu próprio proxy.
P Qual é a diferença entre fixedRate e fixedDelay para tarefas de longa duração?
R 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.
P Como podemos evitar que tarefas agendadas sejam executadas múltiplas vezes em diferentes instâncias em um ambiente de produção?
R 1) Locks de linha no banco de dados (SELECT FOR UPDATE); 2) Locks distribuídos (Redis/ShedLock); 3) Habilitar agendamento em apenas uma instância. Recomendamos a biblioteca ShedLock, que é projetada especificamente para resolver esse problema.
P Como alterar dinamicamente o horário de uma tarefa agendada?
R Você pode usar 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.
P Qual é a diferença entre um método assíncrono que retorna CompletableFuture e um que retorna void?
R Um método 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


📝 Exercícios

  1. 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%