서비스의 인스턴스에 따른 스케쥴러 중복 실행 - ekdan38/HotDealService GitHub Wiki
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의 로그]
[인스턴스 2의 로그]
두 인스턴스 모두 스케쥴러를 실행하고 있음
ShedLock을 사용하여 스케쥴링 Lock 처리
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 테이블 결과]
locked_by의 경우 따로 커스텀 하지 않아서 기본 설정으로 기록 되고 있음