O perigo do @Scheduled do Spring com mais de um pod
Em produção, cada instância roda o seu próprio agendador. Com dois pods, o job executa duas vezes.
Em desenvolvimento, o job roda uma vez por minuto e tudo parece certo. Em produção, você sobe dois pods e o mesmo job passa a rodar duas vezes por minuto: cobrança em duplicidade, e-mail repetido, arquivo processado duas vezes.
Por que acontece
O @Scheduled é local à JVM. Ele não sabe que existe outra instância da aplicação. Cada pod tem o seu agendador e cada um dispara o método no horário.
@Scheduled(cron = "0 */5 * * * *")
public void processarPendentes() {
// roda em TODOS os pods, ao mesmo tempo
pendenteService.processar();
}
Escalar horizontalmente, algo que deveria ser seguro, muda o comportamento do sistema. Esse é o sinal do anti-padrão.
Opções de correção
1. Lock distribuído com ShedLock. Mantém o @Scheduled e garante que só uma instância execute por vez, usando uma tabela no banco (ou Redis) como trava.
@Scheduled(cron = "0 */5 * * * *")
@SchedulerLock(name = "processarPendentes",
lockAtMostFor = "4m",
lockAtLeastFor = "30s")
public void processarPendentes() {
pendenteService.processar();
}
2. Quartz em modo cluster. Mais pesado, mas oferece persistência dos jobs e balanceamento entre nós.
3. Tirar o agendamento da aplicação. Um CronJob do Kubernetes dispara o processamento, que roda uma vez por execução. Faz sentido quando o job não precisa viver dentro do serviço.
Os cuidados que ficam
lockAtMostFormenor que a duração real do job libera o lock no meio da execução e o segundo pod entra. Dimensione com folga.- Relógios diferentes entre os pods desalinham a disputa pelo lock.
- O lock reduz a duplicidade, mas não garante zero. Se o pod cair no meio, o job pode rodar de novo. O processamento precisa ser idempotente.
Regra prática
Todo método agendado deve responder a duas perguntas: "o que acontece se rodar em dois pods?" e "o que acontece se rodar duas vezes?". Se a resposta for "dano", falta lock e falta idempotência.