서비스의 인스턴스에 따른 스케쥴러 중복 실행 - ekdan38/HotDealService GitHub Wiki

1. 문제 상황

Outbox 이벤트를 처리하기위한 스케쥴러가 여러 인스턴스에서 중복 실행되는 문제 발생

문제 상황 예시

  • PaymentService의 인스턴스 2개 실행
  • 결제 처리 이후 결제 결과에 따른 PaymentService -> OrderService 주문 처리 Outbox 스케쥴러 작동
  • 1번의 스케쥴러가 실행되길 기대했지만, 두개의 인스턴스 모두 스케쥴러를 작동시키는 문제 발생
[스케쥴러 코드]
@Component
@Transactional
@RequiredArgsConstructor
@Slf4j(topic = "[OrderStatusOutboxScheduler]")
public class OrderStatusOutboxScheduler {
    private final OrderStatusOutboxRepository outboxRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Scheduled(fixedDelay = 3_000) // 3초 주기
    public void publishOutbox(){
        log.info("OrderStatusOutboxScheduler 실행");
        // 1. outbox DB 조회(오래된 데이터부터 가져와서 순서 보장)
        List<OutboxStatus> statuses = List.of(OutboxStatus.FAILED, OutboxStatus.PENDING);
        List<OrderStatusOutbox> foundOutboxes = outboxRepository.findByOutboxStatusInOrderByIdAsc(statuses);

        // 2. 조회된 outbox FeignClient 이벤트 발행(비동기 처리)
        if(!foundOutboxes.isEmpty()){
            log.info("Order Status 변경 outbox {} 건 진행중", foundOutboxes.size());

            for (OrderStatusOutbox outbox : foundOutboxes) {
                // Status = IN_PROGRESS, tryCount++ 처리;
                outbox.updateToInProgress();
                eventPublisher.publishEvent(outbox);
            }
        }
        log.info("OrderStatusOutboxScheduler 종료");
    }
}

[인스턴스 1의 로그]

Image

[인스턴스 2의 로그]

Image

두 인스턴스 모두 스케쥴러를 실행하고 있음


2. 문제 해결

ShedLock을 사용하여 스케쥴링 Lock 처리

ShedLock 이란?

ShedLock은 DB를 이용해 스케줄러 작업이 여러 인스턴스에서 동시에 실행되지 않도록 락을 걸음

[의존성 추가]

    implementation 'net.javacrumbs.shedlock:shedlock-spring:5.13.0'
    implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.13.0'

[테이블 생성]

CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL,
    locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));

[설정]

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
                JdbcTemplateLockProvider.Configuration.builder()
                        .withJdbcTemplate(new JdbcTemplate(dataSource))
                        .usingDbTime()
                        .build()
        );
    }
}
[수정된 스케쥴러]
@Component
@Transactional
@RequiredArgsConstructor
@Slf4j(topic = "[OrderStatusOutboxScheduler]")
public class OrderStatusOutboxScheduler {
    private final OrderStatusOutboxRepository outboxRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Scheduled(fixedDelay = 3_000) // 3초 주기
    @SchedulerLock(
            name = "OrderStatusOutboxScheduler",
            lockAtMostFor = "10s",
            lockAtLeastFor = "3s")
    public void publishOutbox(){
        log.info("OrderStatusOutboxScheduler 실행");
        // 1. outbox DB 조회(오래된 데이터부터 가져와서 순서 보장)
        List<OutboxStatus> statuses = List.of(OutboxStatus.FAILED, OutboxStatus.PENDING);
        List<OrderStatusOutbox> foundOutboxes = outboxRepository.findByOutboxStatusInOrderByIdAsc(statuses);

        // 2. 조회된 outbox FeignClient 이벤트 발행(비동기 처리)
        if(!foundOutboxes.isEmpty()){
            log.info("Order Status 변경 outbox {} 건 진행중", foundOutboxes.size());

            for (OrderStatusOutbox outbox : foundOutboxes) {
                // Status = IN_PROGRESS, tryCount++ 처리;
                outbox.updateToInProgress();
                eventPublisher.publishEvent(outbox);
            }
        }
        log.info("OrderStatusOutboxScheduler 종료");
    }
}

[ShedLock 테이블 결과]

Image

locked_by의 경우 따로 커스텀 하지 않아서 기본 설정으로 기록 되고 있음

ShekLock을 사용하여 N개의 인스턴스가 N개의 스케쥴러를 작동시키는 문제 해결 -> 1개의 인스턴스만 스케쥴러 작동

⚠️ **GitHub.com Fallback** ⚠️