[Spring Boot] ShedLock이란?
들어가기 전
이번 포스팅에서는 ShedLock에 대해서 다뤄볼 예정입니다. ShedLock은 스케줄러와 연관 있는 Lock입니다.
스프링에서 제공하는 스케줄러가 무엇이며 속성에 대해서 자세히 모르시는 분은 아래 포스팅을 참고하고 이번 포스팅을 읽으시는 것을 추천드리겠습니다.
https://hoestory.tistory.com/91
ShedLock이란?
만약 여러 개의 서버에서 동일한 스케줄링을 실행시키면 원하지 않는 결과가 발생할 수 있습니다.
원하지 않는 결과가 나오는 것을 방지하기 위해 사용하는 것이 ShedLock입니다.
ShedLock은 여러 개의 서버(인스턴스)가 존재할 때 동일한 스케줄링이 반복적으로 수행되지 않도록 Lock을 걸어주는 라이브러리입니다.
지금까지 간단하게 ShedLock에 대해서 알아보았습니다.
이제 ShedLock을 사용하기 위한 테이블, 설정방법에 대해 알아보고 예제를 통해서 ShedLock에 대해서 알아보겠습니다.
테이블 생성
CREATE TABLE shedlock(
name varchar(64) primary key comment '락 이름',
lock_until timestamp(3) null comment '락 유지시간' ,
locked_at timestamp(3) null comment '락 걸린 시간',
locked_by varchar(255) null comment '락 걸린 환경'
);
설정 방법
build.gradle
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.15.1'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.10.0'
ShedLockConfig
@Configuration
@EnableSchedulerLock(defaultLockAtLeastFor = "PT1S", defaultLockAtMostFor = "PT5S")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build()
);
}
}
- EnableSchedulerLock : ShedLock을 사용할 수 있는 환경 제공
- LockProvier : 락 정보를 보관할 수 있는 환경 제공
@SchedulerLock 속성 정보
public @interface SchedulerLock {
String name() default "";
String lockAtMostFor() default "";
String lockAtLeastFor() default "";
}
- SchedulerLock : ShedLcok을 사용할 수 있는 환경 제공
- name : Lock이름
- lockAtMostFor : 최대 락 유지 시간
- lockAtLeastFor : 최소 락 유지 시간
ShedLock을 사용하기 위한 테이블 생성, 설정방법에 대해서 알아보았습니다.
이제 예제를 통해 ShedLock을 알아보겠습니다.
Scheduled - Cron
@Scheduled(cron = "*/10 * * * * *")
@SchedulerLock(name = "cronShedLock", lockAtMostFor = "7s", lockAtLeastFor = "5s")
public void cronShedLock() {
String currentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
//서버 1번일때 수행내용은 서버 1, 서버 2번일때 수행내용은 서버 2
String result = String.format("현재 시간 : %s, 수행내용 : 서버 1 cronShedLock", currentDateTime);
System.out.println(result);
}
위 코드는 매일 10초마다 실행되는 스케줄러입니다.
ShedLock 설정으로는 잠금 최대 유지시간 7초, 잠근 최소 유지 시간 5초로 설정하였습니다.
스케줄러가 각각의 인스턴스(서버)에서 실행되었을 때의 결과는 아래 이미지에서 확인할 수 있습니다.
결과를 확인해 보면 중복되어서 스케줄러가 실행된 것이 아닌 각각의 인스턴스(서버)에서 한 번씩만 실행된 것을 확인할 수 있습니다.
ShedLock 테이블에 데이터가 저장된 모습을 확인할 수 있습니다.
lock_until 값은 lockAtLeast 시간을 5초로 설정한 결과입니다. 이는 작업이 시작된 시점(locked_at)에 최소 잠금 시간인 5초를 더한 시간입니다.
그럼 만약 아래와 같은 상황이 발생하면 어떻게 될까?
- 최대 잠금시간(lockMostAtFor)이 최소 잠금시간(lockAtLeast) 보다 작을 경우
- 현재 작업 중인 스케줄러의 수행시간이 다음 스케줄러가 실행되어야 하는 시간을 초과하여 작업이 끝난 경우
- 현재 작업 중인 스케줄러의 수행시간이 최대 잠금시간(lockMostAtFor)을 초과하는 경우
최대 잠금시간(lockMostAtFor)이 최소 잠금시간(lockAtLeast) 보다 작을 경우
@Scheduled(cron = "*/10 * * * * *")
@SchedulerLock(name = "cronShedLock", lockAtMostFor = "5s", lockAtLeastFor = "7s")
public void cronShedLock() {
String currentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String result = String.format("현재 시간 : %s, 수행내용 : 서버 1 cronShedLock", currentDateTime);
System.out.println(result);
}
최대 잠금시간이 최소 잠금시간보다 작을 경우 스케줄러가 실행될 때 아래와 같은 에러가 발생합니다.
java.lang.IllegalArgumentException: lockAtLeastFor is longer than lockAtMostFor for lock 'cronShedLock'.
발생하는 이유는 최소 잠금시간과 최대 잠금시간을 비교하였을 때 최소 잠금시간이 클 경우 예외가 발생하도록 구현되어 있기 때문입니다.
현재 작업 중인 스케줄러의 수행시간이 다음 스케줄러 실행 되어야 하는 시간을 초과하여 작업이 끝난 경우
@Scheduled(cron = "*/5 * * * * *")
@SchedulerLock(name = "cronShedLock", lockAtMostFor = "7s", lockAtLeastFor = "5s")
public void cronShedLock() throws InterruptedException {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 cronShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 cronShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
위 결과를 확인해 보면 실행 중인 스케줄러의 작업이 다음 스케줄러가 실행되어야 하는 시간을 초과한 상태로 작업이 끝나지 않은 경우 실행되어야 하는 다음 스케줄러는 이전 작업이 끝난 후 설정한 시간 패턴에 맞게 실행됩니다.
현재 작업 중인 스케줄러의 수행시간이 최대 잠금시간(lockMostAtFor)을 초과하는 경우
@Scheduled(cron = "*/5 * * * * *")
@SchedulerLock(name = "cronShedLock", lockAtMostFor = "3s", lockAtLeastFor = "2s")
public void cronShedLock() throws InterruptedException {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 cronShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 cronShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
결과를 확인해 보면 실행 중인 작업이 끝나지 않았음에도 불구하고 다음 스케줄러가 실행이 되어 작업을 수행되는 것을 확인할 수 있습니다.
Scheduled - fixedRate
@Scheduled(fixedRate = 10000)
@SchedulerLock(name = "fixedRateShedLock", lockAtLeastFor = "5s", lockAtMostFor = "7s")
public void fixedRateShedLock() {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 fixedRateShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
}
위 스케줄러는 하나의 작업을 시작하고 지정한 시간(10000) 뒤에 다음 작업을 수행하는 스케줄러입니다.
위 결과를 보면 다음 작업이 이전 작업이 수행된 시간 + 지정된 시간 뒤에 실행되는 것을 확인할 수 있습니다.
두 개의 서버에서 동시에 수행되는 게 아닌 하나의 서버에서 수행되는 것을 확인할 수 있습니다.
현재 작업 중인 스케줄러의 수행시간이 최대 잠금시간(lockMostAtFor)을 초과하는 경우
@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "fixedRateShedLock", lockAtLeastFor = "3s", lockAtMostFor = "5s")
public void fixedRateShedLock() throws InterruptedException {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 fixedRateShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 fixedRateShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
위 결과를 확인해 보면 지금 실행 중인 작업이 다음 작업 시작 시간을 초과하여 수행하고 있을 경우 수행되고 있는 작업이 끝나지도 않은 채 다른 작업도 수행되는 것을 확인할 수 있습니다.
현재 작업 중인 스케줄러의 수행시간이 다음 스케줄러가 실행되어야 하는 시간을 초과하여 작업이 끝난 경우
@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "fixedRateShedLock", lockAtLeastFor = "2s", lockAtMostFor = "7s")
public void fixedRateShedLock() throws InterruptedException {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 fixedRateShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 fixedRateShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
위 결과를 보면 현재 수행 중인 작업이 다음 작업의 예정된 수행 시간을 초과하여 진행되는 경우 현재 작업이 끝나자마자 다음 작업이 즉시 수행되는 것을 확인할 수 있습니다.
Scheduled - fixedDelay
@Scheduled(fixedDelay = 5000)
@SchedulerLock(name = "fixedDelayShedLock", lockAtLeastFor = "2s", lockAtMostFor = "7s")
public void fixedRateShedLock() {
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 fixedDelayShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
}
위 스케줄러는 실행 중인 작업이 끝나고 지정한 시간이 지난 뒤에 다음 스케줄러가 실행됩니다.
두 개의 서버에서 동시에 수행되는 게 아닌 하나의 서버에서 수행되는 것을 확인할 수 있습니다.
현재 작업 중인 스케줄러의 수행시간이 최대 잠금시간(lockMostAtFor)을 초과하는 경우
@Scheduled(fixedDelay = 5000)
@SchedulerLock(name = "fixedDelayShedLock", lockAtLeastFor = "2s", lockAtMostFor = "3s")
public void fixedRateShedLock() throws InterruptedException{
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 fixedDelayShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 1 fixedRateShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
위 결과를 보면 현재 실행 중인 작업이 끝나지도 않은 채 다음 스케줄러 작업이 이 수행되는 현상을 확인할 수 있습니다.
현재 작업 중인 스케줄러의 수행시간이 다음 스케줄러가 실행되어야 하는 시간을 초과하여 작업이 끝난 경우
@Scheduled(fixedDelay = 5000)
@SchedulerLock(name = "fixedDelayShedLock", lockAtLeastFor = "2s", lockAtMostFor = "7s")
public void fixedRateShedLock() throws InterruptedException{
String startCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String startResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 fixedDelayShedLock 시작", startCurrentDateTime);
System.out.println(startResult);
Thread.sleep(6000);
String endCurrentDateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));
String endResult = String.format("현재 시간 : %s, 수행내용 : 서버 2 fixedRateShedLock 끝", endCurrentDateTime);
System.out.println(endResult);
}
결과를 확인해 보면, 현재 실행 중인 작업이 끝난 후에만 다음 작업이 수행됩니다.
하지만 지정된 최소 잠금 시간은 5초임에도 불구하고 실제로는 5초 미만으로 다음 작업이 수행되는 것을 확인할 수 있습니다.
이는 ShedLock 설정과 관련이 있습니다.
ShedLock의 설정인 최대 잠금 시간은 스케줄러가 실행될 때 데이터에 삽입됩니다.
그래서 스케줄러 작업이 오래 걸릴 경우 다음 작업의 실행 시간이 설정된 시간과 일치하지 않을 수 있습니다.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] @Scheduled에 대하여 (1) | 2024.12.20 |
---|---|
[Spring Boot] 트랜잭션 전파과정에 대하여 (0) | 2024.11.10 |
[RabbitMQ] RabbitMQ 개념 및 Spring 연동 (0) | 2024.09.18 |
[Spring] Checked Exception, Unchecked Exception의 트랜잭션 처리 방식 (2) | 2024.06.26 |
[Spring] @DataJpaTest에서 Auditing 적용 안되는 현상 (0) | 2024.05.21 |
댓글
이 글 공유하기
다른 글
-
[Spring Boot] @Scheduled에 대하여
[Spring Boot] @Scheduled에 대하여
2024.12.20 -
[Spring Boot] 트랜잭션 전파과정에 대하여
[Spring Boot] 트랜잭션 전파과정에 대하여
2024.11.10 -
[RabbitMQ] RabbitMQ 개념 및 Spring 연동
[RabbitMQ] RabbitMQ 개념 및 Spring 연동
2024.09.18 -
[Spring] Checked Exception, Unchecked Exception의 트랜잭션 처리 방식
[Spring] Checked Exception, Unchecked Exception의 트랜잭션 처리 방식
2024.06.26