본문 바로가기

SpringFramework/Spring

Spring - 스케줄러 중복실행 이슈분석

Spring 프레임워크에는 linux에서 제공하는 배치(일정주기 혹은 특정시간에 자동실행)와 같은 라이브러리를 제공한다. 이를 스프링 스케줄러라고 한다. 배치프로그램은 실무에서 필수적으로 굉장히 많이 쓰이며 스프링에서는 간단하게부터 시작해서 스프링 배치 프레임워크와 연계하여 안정적인 대용량처리 프로그램을 작성할 수 있다. (스케줄러와 스프링 배치는 같은 개념이 아니다! 스케줄러로 스프링 배치를 수행한다)

스프링 스케줄러

Spring 프레임워크 (spring-boot-starter) 내에 기본적으로 포함되어있으므로 의존성 추가는 필요하지 않다. (Quarts를 사용하는 경우에는 의존성추가 필요. 하지만 이번 글에서는 다루지 않는다)

 

기본적인 사용방법이다.

import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;

@Controller
@EnableScheduling
public class SchedulerController {
    @Scheduled(cron = "*/20 * * * * *") // 매 20초마다 Task 수행
   public void schedulerStart1() {
        System.out.println("Hello! scheduler 1");
    }

    @Scheduled(fixedDelay = 2000) // 스케줄러가 끝나는 시간 기준으로 2000ms (2초) 간격으로 Task 수행
    public void schedulerStart2() {
        System.out.println("Hello! scheduler 2");
    }

    @Scheduled(fixedRate = 3000) // 스케줄러가 시작하는 시간 기준으로 3000ms (3초) 간격으로 Task 수행
    public void schedulerStart3() {
        System.out.println("Hello! scheduler 3");
    }
}

 

스케줄러가 아래와같이 실행되는 모습을 확인할 수 있다.

 

스케줄러에서는 cron 표현식 이외에 다양한 job 실행 주기를 설정할 수 있다. 인자내용을 알아보자.

인자 내용
cron cron표현식을 사용한다. [초 분 시 일 월 주 (년)]으로 표현한다.
fixedDelay milliseconds 단위로, 이전 작업이 끝난 시점으로 부터 고정된 시간을 설정한다.
fixedDelayString fixedDelay와 동일하나, property의 value를 문자열로 입력한다.
fixedRate milliseconds 단위로, 이전 작업이 수행되기 시작한 시점으로 부터 고정된 시간을 설정한다.
fixedRateString fixedRate와 동일하나, property의 value를 문자열로 입력한다.
initialDelay 스케줄러에서 메서드가 등록되자마자 수행하는 것이 아닌 초기 지연시간을 설정하는 것이다.
initialDelayString initialDelay와 동일하나 property의 value를 문자열로 입력한다.
zone cron표현식을 사용했을 때 사용할 time zone으로 따로 설정하지 않으면 기본적으로 서버의 time zone이다.

스프링 스케줄러 중복실행 이슈

만약, 이러한 스케줄러 서버가 여러대(멀티 서버)가 있다면, 어떻게 될까? 아마 배치가 서버 대수만큼 동일하게 실행될 것이며, DB처리 수행시 중복처리가 되거나, Lock이 걸려서 정상적으로 데이터 처리가 안되는 등의 이슈가 발생할 수 있다. 이런 문제점을 해결하기위해서는 어떤 방법을 사용할 수 있을까?

 

이를 방지하기 위해 ShedLock이라는 것을 사용하여 해결할 수 있다. ShedLock은 의존성 추가가 필요하다.

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

(이 경우에는 DB처리를 제어하기 위해 추가된 경우고, NoSQL, ElasticSearch 등 다른 솔루션을 처리하려면 다른 provider가 추가되면 될 것이다)

더 알아보기(shedlock github) => https://github.com/lukas-krecan/ShedLock

Bean 등록

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

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

Lock기능에 필요한 테이블 생성

CREATE TABLE `shedlock` (
    `name` VARCHAR(64) NOT NULL COMMENT '스케줄잠금 name',
    `lock_until` TIMESTAMP(3) NOT NULL COMMENT '잠금기간',
    `locked_at` TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '잠금일시',
    `locked_by` VARCHAR(255) NOT NULL COMMENT '잠금신청자',
    PRIMARY KEY (`name`))
COMMENT = '스케줄잠금';

해당 테이블에 스케줄과 lock에 대한 정보가 삽입/갱신 된다.

(!) 위 테이블의 데이터를 수동으로 삭제하면 안된다. ShedLock은 인 메모리 캐시를 지니고 있기 때문에 애플리케이션이 다시 시작될 때 까지 row를 재생성하지 않는다. (다시 말해 row가 수동으로 삭제될 경우 lock에 대한 update 정보가 누락된다)

(Test환경 및 로컬환경에서는 애플리케이션이 종료된 상태에서 삭제하자)

ShedLock 사용

import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;

@Controller
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S", defaultLockAtLeastFor = "PT30S") // (기본 세팅)30초동안 Lock
public class SchedulerController {
    @Scheduled(cron = "*/20 * * * * *") // 매 20초마다 Task 수행
    @SchedulerLock(name = "schedulerStart1", lockAtMostFor = "PT19S", lockAtLeastFor = "PT19S")
   public void schedulerStart1() {
        System.out.println("Hello! scheduler 1");
    }

    @Scheduled(fixedDelay = 2000) // 스케줄러가 끝나는 시간 기준으로 2000ms (2초) 간격으로 Task 수행
    public void schedulerStart2() {
        System.out.println("Hello! scheduler 2");
    }

    @Scheduled(fixedRate = 3000) // 스케줄러가 시작하는 시간 기준으로 3000ms (3초) 간격으로 Task 수행
    public void schedulerStart3() {
        System.out.println("Hello! scheduler 3");
    }
}

@EnableSchedulerLock 어노테이션 추가. 각 메소드에 @SchedulerLock 어노테이션 사용

 

@SchedulerLock 어노테이션 속성값들에 대해서 알아보자.

속성 내용
name 스케줄 작업의 고유 이름. ShedLock 테이블의 name 컬럼으로 기본키 역할을 하게 되므로 스케줄 작업의 고유한 이름을 입력해야 한다.
lockAtLeastFor 작업이 Lock 되어야 할 최소한의 시간을 입력한다. 짧은 작업일 경우에는 노드간의 클럭 차이로 중복 실행되는 것을 막기위해 사용한다
lockAtMostFor 작업을 진행 중인 노드가 소멸될 경우에도 Lock이 유지될 시간을 입력한다. 해당 시간은 실제 작업에 소요되는 시간보다 훨씬 길게 해야 한다. (입력하지 않으면 @EnableSchedulerLock의 디폴트 값으로 세팅)

Lock 시간은 보통 권장주기 -1이다.

ShedLock 테이블 확인

2022-03-14 20:20:20 ~ 2022-03-14 20:28:39 까지 Lock이 yunsang.local에 의해 걸려있는 상태이며 최초로 INSERT

schedulerStart1 이름의 2022-03-14 20:30:00 ~ 2022-03-14 20:30:19 까지 Lock이 yunsang.local에 의해 걸려있는 상태. 시간데이터가 업데이트 되었다.

참고자료

= https://jeong-pro.tistory.com/186

= https://github.com/lukas-krecan/ShedLock

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

= https://velog.io/@recordsbeat/%EB%A9%80%ED%8B%B0-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%8A%A4%EC%BC%80%EC%A4%84-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-ShedLock

 

'SpringFramework > Spring' 카테고리의 다른 글

Spring - PSA  (0) 2022.04.07
Spring - Mybatis 샵(#)과 달러($)의 차이  (0) 2022.03.10
Spring - Mybatis FrameWork 여러 스키마 적용하기  (0) 2022.02.08
Spring - 인스턴스 변수 참조  (0) 2021.10.20
Spring - Redis 연동  (0) 2021.10.20