본문 바로가기

Web

[ spring ] 스케줄러 중복 실행 이슈 및 해결

반응형


💡 배경

프로젝트를 진행하면서 스케쥴러를 통해 정해진 시간에 주기적으로 

REST API 데이터와 DB 데이터의 정합성을 맞춰야 하는 작업이 필요했다.
이 때 여러 대의 서버 인스턴스에서 동일한 서버 애플리케이션이 구동되고 있다면, (ex.톰캣 이중화)
스케쥴러가 여러 서버에서 동시에 실행되었을 때 중복작업 이슈가 발생할 수 있다는 것을 알았다.
그래서 스케쥴러 작업이 하나의 서버에서만 수행되도록 해주었다.

 


✅ 방법

1. Spring Batch + Quartz + JDBC
2. Redis 또는 DB 기반 분산락
3. 외부 잡 매니저 (e.g., Airflow, Jenkins, AWS EventBridge)

이 중 내가 선택한 방식은 2번의 DB 기반 분산락 방식이었다.

 


| DB 분산락이란?

락 전용 테이블을 사용해서 특정 작업의 실행 권한을 DB에 먼저 선점합니다.
선점에 성공한 인스턴스만 스케줄을 실행합니다.
락은 트랜잭션 or 시간 기반으로 해제됩니다.

 


| 프로젝트 적용

1. 락 테이블 생성 SQL

CREATE TABLE distributed_lock (
    lock_key VARCHAR(100) PRIMARY KEY, /*락 식별자*/
    locked_until TIMESTAMP, /*락 만료 시간*/
    locked_by VARCHAR(100) /*락 소유자 (서버 IP 등)*/
);



2. Spring + Mybatis

<!-- Mapper XML -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="cohttp://m.example.mapper.DistributedLockMapper">

    <update id="tryLock">
        UPDATE distributed_lock
        SET locked_until = #{lockedUntil}, locked_by = #{lockedBy}
        WHERE lock_key = #{lockKey}
        AND (locked_until IS NULL OR locked_until &lt; #{now})
    </update>

    <update id="releaseLock">
        UPDATE distributed_lock
        SET locked_until = NULL
        WHERE lock_key = #{lockKey}
        AND locked_by = #{lockedBy}
    </update>

</mapper>

 

반응형



3. Service

@Service
public class LockService {

    @Autowired
    private DistributedLockMapper lockMapper;

    public boolean tryLock(String lockKey, Duration holdDuration) {
        String lockedBy = InetAddress.getLocalHost().getHostName();
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime until = now.plus(holdDuration);

        int updated = lockMapper.tryLock(lockKey, until, lockedBy, now);
        return updated > 0;
    }

    public void releaseLock(String lockKey) {
        String lockedBy = InetAddress.getLocalHost().getHostName();
        lockMapper.releaseLock(lockKey, lockedBy);
    }
}



4. 스케쥴러

@Scheduled(cron = "0 0 * * * *")
public void myScheduledTask() {
    if (lockService.tryLock("my-task", Duration.ofMinutes(10))) {
        try {
            // 실제 작업 실행
            runMyTask();
        } finally {
            lockService.releaseLock("my-task");
        }
    } else {
        // 다른 인스턴스가 이미 수행 중
    }
}

 



| 테스트

runMyTask()에서 특정 테이블에 데이터 값을 insert 하는 작업을 구현한 다음 테스트를 진행해보았다.
톰캣 2개를 띄워두고 테스트 해보았는데..각각의 톰캣에서 모두 insert 작업이 된 것이 아닌가..?
그래서 원익을 파악해보니 아래와 같았다.

 


| 이슈 및 해결

1. 왜 이런 일이 발생하나?
대부분의 DB 기반 분산 락 구현은 다음과 같은 방식으로 동작한다.

1) 스케줄러 실행 시 DB에 락 획득 시도 (SELECT ... FOR UPDATE 또는 INSERT)
2) 성공하면 작업 수행
3) 작업이 끝나면 락 해제 (UPDATE, DELETE 등)

🔴 작업이 너무 빠르게 끝나면
두 인스턴스가 거의 같은 시간에 락을 요청
=> 락 획득과 해제가 너무 빨라 둘 다 통과하게 되는 "레이스 컨디션" 발생

2. 해결
runMyTask() 실행 전에 강제로 sleep 을 넣어 인위적으로 시간을 확보하여 락 유지 시간을 조절했다.

찜찜..

 


| 개선

ShedLock (Spring Scheduler + JDBC/Redis 기반 락을 쉽게 적용 가능) 방식을 통해 
레이스 컨디션을 방지하도록 개선할 예정이다.

 

▼ ShedLock 해결 보러가기 ▼

 

[ spring ] ShedLock 스케줄러 중복 실행 이슈 해결

✅ 개발환경Java 8 Spring Boot 2.7 ShedLock 4.44.0 MySQL (JDBC Template 기반) 초반에 java8인데 ShedLock 5.10.1 버전 사용하려해서 계속 에러 이슈 있었다.. 후 | Gradle 의존성 (build.gradle)dependencies { implementation 'net.jav

im-codding.tistory.com

 

반응형