Shedlock을 사용해서 Scheduler 중복 실행 방지하기

uni.gy·2025년 11월 10일
0

멀티 서버, 멀티 스레드 환경에서도 스케줄러를 하나만 실행하기 위해 Shedlock을 사용했습니다.

필요 의존성

implementation 'net.javacrumbs.shedlock:shedlock-spring:6.10.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:6.10.0'

SchedulingConfig

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtLeastFor = "10s", defaultLockAtMostFor = "90s") //Lock 이 유지되는 최소,최대 시간
public class SchedulingConfig {

    @Value("${spring.profiles.active}")
    private String profile;

    @Value("${spring.application.name}")
    private String appName;

    @Bean
    public LockProvider lockProvider(RedisConnectionFactory redisConnectionFactory) {
        String prefix = appName + ":" + profile;
        return new RedisLockProvider(redisConnectionFactory, prefix);
    }
}

defaultLockAtLeastFor : Lock을 유지하는 최소 시간 기본 설정
defaultLockAtMostFor : Lock을 유지하는 최대 시간 기본 설정
해당 코드는 RedisLockProvider를 사용하고 있습니다.

Scheduler

@Component
@RequiredArgsConstructor
@Slf4j
public class HubRouteInfoScheduler {

    private final HubRepository hubRepository;
    private final HubRouteInfoRepository routeInfoRepository;
    private final HubService hubService;
    private final HubRouteInfoRepository hubRouteInfoRepository;

    //기본 설정 2시간마다
    @Scheduled(
        fixedRateString = "${scheduler.route-infos.fixed-rate}",
        initialDelayString = "${scheduler.route-infos.initial-delay}"
    )
    // 멀티 서버, 멀티 스레드 환경에서도 스케줄러를 하나만 실행 / lock 유지 최소, 최대 시간
    @SchedulerLock(
        name = "generateMissingHubRouteInfos",
        lockAtLeastFor = "${shedlock.route-infos.at-least}",
        lockAtMostFor  = "${shedlock.route-infos.at-most}"
    )
    public void generateMissingRoutes() {
        log.info("[{}] HubRouteInfoScheduler started at {}", LocalDateTime.now(), Thread.currentThread().getName());
        long start = System.currentTimeMillis();
        int createdCount = 0;

        List<UUID> hubIds = hubRepository.getActiveHubIds();
        if (hubIds.size() < 2) return;

        Set<RoutePairDto> existing = new HashSet<>(routeInfoRepository.findExistingParisIn(hubIds));

        // 전체 순서쌍(출발!=도착) 생성 → 차집합으로 미존재 목록 계산
        List<RoutePairDto> missing = new ArrayList<>();
        for (UUID dep : hubIds) {
            for (UUID arr : hubIds) {
                if (!dep.equals(arr)) {
                    RoutePairDto pair = new RoutePairDto(dep, arr);
                    if (!existing.contains(pair)) {
                        missing.add(pair);
                    }
                }
            }
        }
        log.debug("missing size : {}", missing.size());

        Map<UUID, Hub> hubCache=hubService.getHubByIds(hubIds).stream()
            .collect(Collectors.toMap(Hub::getHubId, Function.identity()));

        for (RoutePairDto pair : missing) {
            try {
                Hub departureHub = hubCache.get(pair.departureId());
                Hub arrivalHub = hubCache.get(pair.arrivalId());

                Long durationMin = DistanceTimeUtil.estimateDurationMinutes(departureHub.getLatitude(),departureHub.getLongitude(),arrivalHub.getLatitude(),arrivalHub.getLongitude());
                Double distanceKm = DistanceTimeUtil.calculateDistanceKm(departureHub.getLatitude(),departureHub.getLongitude(),arrivalHub.getLatitude(),arrivalHub.getLongitude());

                HubRouteInfo routeInfo = HubRouteInfo.create(pair.departureId(),pair.arrivalId(),durationMin,distanceKm);
                hubRouteInfoRepository.save(routeInfo);
                createdCount++;
            } catch (BusinessException e) {
                log.warn("ErrorCode : {}, Message: {}",e.getErrorCode(),e.getMessage());
            } catch (DataIntegrityViolationException e) {
                log.warn("유니크 제약 조건 위반 departureId: {}, arrivalId: {}",pair.departureId(),pair.arrivalId());
            } catch (Exception e) {
                 log.warn("Route create failed: {} -> {}", pair.departureId(), pair.arrivalId(), e);
            }
        }

        long elapsed = System.currentTimeMillis() - start;
        log.info("[HubRouteInfoScheduler] finished at {} — created {} new routes in {} ms",
            LocalDateTime.now(), createdCount, elapsed);
    }
}

@SchedulerLock 어노테이션을 통해 shedlock을 적용할 수 있습니다.

참고

profile
한결같이

0개의 댓글