Операции сохранения не фиксируются при использовании аннотации @Scheduled

Эта служба Spring, EmailsOutgoingService, отвечает за обработку исходящих писем и запланирована на запуск каждые 60 секунд с использованием аннотации @Scheduled(fixedRate = 60000). По сути, он проверяет статус электронного письма в очереди и выполняет операции с существующей записью или создает другую. Проблема в том, что транзакция «repository.save» не фиксируется в базе данных.

@Service
public class EmailsOutgoingService {

    @Autowired
    private JavaMailSender emailSender;

    @Autowired
    private EmailsOutgoingRepository repository;

    @Scheduled(fixedRate = 60000)
    @Transactional
    public void processEmails() {
        System.out.println("Processing emails...");
        List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
        List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);

        for (EmailsOutgoing pendingEmail : pendingEmails) {
            System.out.println("Processing pending email with ID: " + pendingEmail.getId());
            sendEmail(pendingEmail);
        }

        for (EmailsOutgoing failedEmail : failedEmails) {
            System.out.println("Processing failed email with ID: " + failedEmail.getId());
            retryFailedEmail(failedEmail);
        }
    }

    public void sendEmail(EmailsOutgoing email) {
        try {
            System.out.println("Sending email...");
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(email.getEmailReceivers().split(","));
            message.setSubject(email.getEmailSubject());
            message.setText(email.getEmailBody());
            emailSender.send(message);
            email.setDeliveryStatus(EmailsOutgoing.STATUS_DELIVERED);
            repository.save(email);
            System.out.println("Email sent successfully, status updated to DELIVERED with ID: " + email.getId());
        } catch (Exception e) {
            System.out.println("Failed to send email: " + e.getMessage());
            email.setDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
            email.setEmailMessage(e.getMessage());
            repository.save(email);
            System.out.println("Email failed, status updated to FAILED with ID: " + email.getId());

            EmailsOutgoing newEmail = new EmailsOutgoing();
            newEmail.setEmailSubject(email.getEmailSubject());
            newEmail.setEmailBody(email.getEmailBody());
            newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
            newEmail.setEmailEntity(email.getEmailEntity());
            newEmail.setClientCode(email.getClientCode());
            newEmail.setEmailReceivers(email.getEmailReceivers());
            repository.save(newEmail);

            System.out.println("Created new pending email with ID: " + newEmail.getId());
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void retryFailedEmail(EmailsOutgoing failedEmail) {
        System.out.println("Retrying failed email...");
        EmailsOutgoing newEmail = new EmailsOutgoing();
        newEmail.setEmailSubject(failedEmail.getEmailSubject());
        newEmail.setEmailBody(failedEmail.getEmailBody());
        newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
        newEmail.setEmailEntity(failedEmail.getEmailEntity());
        newEmail.setClientCode(failedEmail.getClientCode());
        newEmail.setEmailReceivers(failedEmail.getEmailReceivers());

        repository.save(newEmail);
        System.out.println("Created new pending email with ID: " + newEmail.getId());

        sendEmail(newEmail);
    }
}

Если я попытаюсь вызвать метод почтальона, все будет работать нормально. Я пришел к выводу, что @Scheduled не совершает транзакцию. Как можно решить проблему? Я также попытался создать отдельный класс с помощью @Scheduled, который вызывает класс обслуживания, выполняющий операции сохранения, но он не работает:

Вызов метода с помощью @Scheduler из другого класса:

Планировщик исходящих писем:

@Component
public class EmailsOutgoingScheduler {

    @Autowired
    private EmailsOutgoingService emailsOutgoingService;

    @Scheduled(fixedRate = 60000) 
    public void checkForNewNotifications() {
        System.out.println("Checking for new notifications...");
        emailsOutgoingService.processEmails();
    }
}

Служба исходящих сообщений электронной почты:

@Service
public class EmailsOutgoingService {

    @Autowired
    private JavaMailSender emailSender;

    @Autowired
    private EmailsOutgoingRepository repository;

    public void processEmails() {
        System.out.println("Processing emails...");
        List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
        List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);

        for (EmailsOutgoing pendingEmail : pendingEmails) {
            System.out.println("Processing pending email with ID: " + pendingEmail.getId());
            sendEmail(pendingEmail);
        }

        for (EmailsOutgoing failedEmail : failedEmails) {
            System.out.println("Processing failed email with ID: " + failedEmail.getId());
            retryFailedEmail(failedEmail);
        }
    }

    @Transactional
    public void sendEmail(EmailsOutgoing email) {
        try {
            System.out.println("Sending email...");
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(email.getEmailReceivers().split(","));
            message.setSubject(email.getEmailSubject());
            message.setText(email.getEmailBody());
            emailSender.send(message);
            email.setDeliveryStatus(EmailsOutgoing.STATUS_DELIVERED);
            repository.save(email);  // Update status to delivered
            System.out.println("Email sent successfully, status updated to DELIVERED with ID: " + email.getId());
        } catch (Exception e) {
            System.out.println("Failed to send email: " + e.getMessage());
            email.setDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
            email.setEmailMessage(e.getMessage());
            repository.save(email);  // Update status to failed
            System.out.println("Email failed, status updated to FAILED with ID: " + email.getId());

            // Create a new pending email record to retry
            EmailsOutgoing newEmail = new EmailsOutgoing();
            newEmail.setEmailSubject(email.getEmailSubject());
            newEmail.setEmailBody(email.getEmailBody());
            newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
            newEmail.setEmailEntity(email.getEmailEntity());
            newEmail.setClientCode(email.getClientCode());
            newEmail.setEmailReceivers(email.getEmailReceivers());
            repository.save(newEmail);

            System.out.println("Created new pending email with ID: " + newEmail.getId());
        }
    }

    @Transactional
    public void retryFailedEmail(EmailsOutgoing failedEmail) {
        System.out.println("Retrying failed email...");
        // The retry logic here is to create a new pending email record and then try sending it
        EmailsOutgoing newEmail = new EmailsOutgoing();
        newEmail.setEmailSubject(failedEmail.getEmailSubject());
        newEmail.setEmailBody(failedEmail.getEmailBody());
        newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
        newEmail.setEmailEntity(failedEmail.getEmailEntity());
        newEmail.setClientCode(failedEmail.getClientCode());
        newEmail.setEmailReceivers(failedEmail.getEmailReceivers());

        repository.save(newEmail);
        System.out.println("Created new pending email with ID: " + newEmail.getId());

        sendEmail(newEmail);
    }
}

Журнал гибернации:

Checking for new notifications...
[restartedMain] INFO org.springframework.boot.devtools.autoconfigure.ConditionEvaluationDeltaLoggingListener - Condition evaluation unchanged
Processing emails...
[2024-06-19 17:13:32.011] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select emailsoutg0_.pk_email_outgoing as pk_email1_11_, emailsoutg0_.client_code as client_c2_11_, emailsoutg0_.email_outgoing_status as email_ou3_11_, emailsoutg0_.email_outgoing_body as email_ou4_11_, emailsoutg0_.email_outgoing_date as email_ou5_11_, emailsoutg0_.email_outgoing_entity as email_ou6_11_, emailsoutg0_.email_outgoing_message as email_ou7_11_, emailsoutg0_.email_outgoing_receivers as email_ou8_11_, emailsoutg0_.email_outgoing_subject as email_ou9_11_ from emails_outgoing emailsoutg0_ where emailsoutg0_.email_outgoing_status=?
[2024-06-19 17:13:32.034] - 23980 TRACE [scheduling-1] --- org.hibernate.type.descriptor.sql.BasicBinder: binding parameter [1] as [VARCHAR] - [Pending]
[2024-06-19 17:13:32.037] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select emailsoutg0_.pk_email_outgoing as pk_email1_11_, emailsoutg0_.client_code as client_c2_11_, emailsoutg0_.email_outgoing_status as email_ou3_11_, emailsoutg0_.email_outgoing_body as email_ou4_11_, emailsoutg0_.email_outgoing_date as email_ou5_11_, emailsoutg0_.email_outgoing_entity as email_ou6_11_, emailsoutg0_.email_outgoing_message as email_ou7_11_, emailsoutg0_.email_outgoing_receivers as email_ou8_11_, emailsoutg0_.email_outgoing_subject as email_ou9_11_ from emails_outgoing emailsoutg0_ where emailsoutg0_.email_outgoing_status=?
[2024-06-19 17:13:32.038] - 23980 TRACE [scheduling-1] --- org.hibernate.type.descriptor.sql.BasicBinder: binding parameter [1] as [VARCHAR] - [Failed]
Processing pending email with ID: 1
Sending email...
Email failed, status updated to FAILED with ID: 1
[2024-06-19 17:13:32.122] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select nextval ('emails_outgoing_sequence')
Created new pending email with ID: 866

1
0
76
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Однажды я попал в такую ​​же ловушку.

Транзакция не распространяется через прокси-сервер Spring, и управление транзакциями не происходит. Вам нужно вызвать общедоступный метод, помеченный @Transactional, из другого класса.

@Autowired
private EmailProcessor email processor;

@Scheduled(fixedRate = 60000)
public void processEmails() {
    System.out.println("Processing emails...");
    emailProcessor.processEmails();
}
@Service
public class EmailProcessor {

    // ...

    @Transactional
    public void processEmails() {
        List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
        List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);

        for (EmailsOutgoing pendingEmail : pendingEmails) {
            System.out.println("Processing pending email with ID: " + pendingEmail.getId());
            sendEmail(pendingEmail);
        }

        for (EmailsOutgoing failedEmail : failedEmails) {
            System.out.println("Processing failed email with ID: " + failedEmail.getId());
            retryFailedEmail(failedEmail);
        }
    }

    public void sendEmail(EmailsOutgoing email) {
        // code
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void retryFailedEmail(EmailsOutgoing failedEmail) {
        // code
    }
}

Привет, спасибо, что ответили. Это именно то, что я пытался сделать вторым подходом, но он не работает. Я обновил вопрос

Salvatore Montagna 19.06.2024 11:50

Даже этот второй подход лишь увеличивает последовательность первичного ключа, но не фиксирует

Salvatore Montagna 19.06.2024 12:04

Не могли бы вы предоставить некоторые журналы отладки из Hibernate?

Nikolas Charalambidis 19.06.2024 16:36

Я снова обновил вопрос с журналом спящего режима

Salvatore Montagna 19.06.2024 17:14

Весь processEmails должен быть @Transactional, потому что в противном случае нетранзакционный EmailsOutgoingService#processEmails вызов @Transactional методов в том же классе фактически обходит контроль управления транзакциями, поэтому аннотации sendEmail и retryFailedEmail игнорируются. Вы можете использовать TransactionTemplate, чтобы обернуть тела методов sendEmail и retryFailedEmail для более точного управления транзакциями.

Nikolas Charalambidis 20.06.2024 09:10

Даже попытка с TransactionTemplate не сработала. У меня тот же журнал Hibernate

Salvatore Montagna 20.06.2024 12:20
Ответ принят как подходящий

Решение

Для тех, кто попал в ловушку использования @Scheduled для обновления данных в базе данных без решения:

Я решил обойти JPA, используя две функции PostgreSQL для создания и обновления записей. В сервисе ничего не изменилось, я просто вызываю функции из репозитория, используя аннотацию @Procedure.

    @Transactional
    @Procedure(procedureName = "updateOutgoingEmails")
    int updateOutgoingEmails(@Param("p_id") Integer id, 
                             @Param("p_deliveryStatus") String deliveryStatus, 
                             @Param("p_emailMessage") String emailMessage);
    
    @Transactional
    @Procedure(procedureName = "createOutgoingEmails")
    int createOutgoingEmails(@Param("p_emailSubject") String emailSubject,
                             @Param("p_emailBody") String emailBody,
                             @Param("p_emailEntity") String emailEntity,
                             @Param("p_clientCode") String clientCode,
                             @Param("p_emailReceivers") String emailReceivers,
                             @Param("p_deliveryStatus") String deliveryStatus, 
                             @Param("p_emailMessage") String emailMessage);
   

Метод обновления должен быть недействительным, но при использовании аннотации @Modifying возникли ошибки, поэтому я решил сохранить возвращаемое значение, чтобы реализовать это раз и навсегда.

Другие вопросы по теме

Похожие вопросы