본문으로 바로가기

회사 프로젝트인 부릉마켓이라는 식자재 이커머스 서비스를 개발했고 얼마전 무사히 오픈했다. 근데 운영업무를 진행하다가 특이한 이슈가 발생했다.. 사용자는 주문시 배송받을 날짜를 선택할 수 있고 배송날짜는 내일 ~ 3일후 날짜까지 나타난다.

 

 

오늘을 기준으로 동적으로 DB에 배송날짜를 만들어줘야하기 때문에 스케줄러를 활용해서 매일 새벽1시에 배송날짜를 만드는 작업을 실행한다.

 

// Scheduler 

// 매일 새벽 1시에 배송날짜 생성
    @Scheduled(cron = "0 0 1 * * *", zone = "Asia/Seoul")
    public void createShippingDate() {
        try {
            log.info("배송날짜 스케줄러 실행");
            shippingSlotService.createShippingDate(LocalDate.now());
        } catch (Exception e) {
            log.error("ShippingSlotScheduler >> createShippingDate 실패 : {}", e);
        }
    }

 

 

// Service. 오늘 ~ 6일후 날짜까지 insert 한다.

@Transactional(rollbackFor = Exception.class)
    public void createShippingDate(LocalDate startDate) {

        try {
            for (int plusDay=0; plusDay<7; plusDay++) {
                LocalDate targetDate = startDate.plusDays(plusDay);
                ShippingDate shippingDate = shippingDateRepository.findByShippingDate(targetDate);

                if (shippingDate == null) {
                    //TODO 공휴일 지정
                    ShippingDate newShippingDate = ShippingDate
                            .builder()
                            .shippingDate(targetDate)
                            .week(targetDate.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.KOREAN))
                            .build();

                    shippingDateRepository.save(newShippingDate);
                }
            }
        } catch (Exception e) {
            throw e;
        }
    }

 

 

로컬에서는 별 문제없었는데,, 운영 배포한 뒤 간헐적으로 중복으로 insert되는 문제가 발생했다.

 

???

2022-07-14이 두 번 생성됐네?? 이슈 재연을 하려고 몇번을 테스트해봐도 문제를 찾을순 없었고, 구글링을 해보니 애플리케이션 서버가 2대 이상일경우 스케줄러가 중복으로 실행될수 있다고 한다. 말로만 듣던 동시성 문제라는게 이런건가??

 

 

이러한 중복된 스케줄러 실행을 막기위해 ShedLock이라는 라이브러리를 이용해 스케줄러에 Lock을 거는 방법이 있다고 한다. 

 

/* Lock 관련 테이블 생성. 스케줄러 annotation에 적용한 name이 pk로 생성된다. */


CREATE TABLE `shedlock` (
  `name` varchar(64) NOT NULL COMMENT '스케줄잠금이름',
  `lock_until` timestamp(3) NULL DEFAULT NULL COMMENT '잠금기간',
  `locked_at` timestamp(3) NULL DEFAULT NULL COMMENT '잠금일시',
  `locked_by` varchar(255) DEFAULT NULL COMMENT '잠금신청자',
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

 

// build.gradle

implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-spring', version: '4.24.0'
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-provider-jdbc-template', version: '4.24.0'

 

 

// Configuration

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT50S")
public class ShedLockConfiguration {

  @Bean
  public LockProvider lockProvider(DataSource dataSource) {
    return new JdbcTemplateLockProvider(dataSource);
  }
}

 

 

/*
수정된 Scheduler. @SchedulerLock 어노테이션 추가.
*/

@Slf4j
@Component
@EnableScheduling
public class ShippingSlotScheduler {

    private ShippingSlotService shippingSlotService;
    private static final String ONE_MIN = "PT1M";

    public ShippingSlotScheduler(ShippingSlotService shippingSlotService) {
        this.shippingSlotService = shippingSlotService;
    }

    // 매일 새벽 1시에 배송날짜 생성
    @Scheduled(cron = "0 0 1 * * *", zone = "Asia/Seoul")
    @SchedulerLock(name = "CREATE_SHIPPING_DATE", lockAtLeastFor = ONE_MIN, lockAtMostFor = ONE_MIN)
    public void createShippingDate() {
        try {
            log.info("배송날짜 스케줄러 실행");
            shippingSlotService.createShippingDate(LocalDate.now());
        } catch (Exception e) {
            log.error("ShippingSlotScheduler >> createShippingDate 실패 : {}", e);
        }
    }
}

 

 

위 코드를 적용하고 스케줄러가 실행하면 아래와 같이 shedlock 테이블에 데이터가 insert되는걸 볼 수 있다.

 

참고 사이트

https://steady-snail.tistory.com/174

https://jsonobject.tistory.com/601

 

Spring Boot, ShedLock, 멀티 노드 환경에서 특정 스케쥴 작업의 중복 실행 방지하기

개요 Spring Boot에서 특정 빈의 메써드에 지정된 @Scheduled 는 특정 시간 또는 주기로 애플리케이션 로직이 실행될 수 있도록 해준다. @Scheduled에서 발생할 수 있는 주요 이슈로, 분산 시스템의 개념

jsonobject.tistory.com

 

[Spring Framework] 멀티 서버에서 Spring 스케줄러 중복실행 방지

Spring의 @Scheduled 어노테이션은 Spring Framework에서 쉽게 cron을 구성할 수 있는 유용한 기능입니다. 그런데 WAS를 늘리게 되면 상당히 골치아픈 상황이 벌어집니다. cron작업이 WAS마다 실행되기 때문에

steady-snail.tistory.com