Redis 분산 락을 이용한 스케줄러 리더 선출 개선기

이경헌·2025년 5월 20일
0

기존 블루/그린 배포 환경에서는 특정 포트(8101, 8103)에서만 스케줄러가 실행되도록 했습니다. 하지만 두 서버가 동시에 떠있으면 둘 다 작업을 수행하는 중복 문제가 있었습니다.
이를 해결하기 위해 Redis 분산 락(Distributed Lock) 패턴을 도입했습니다.


✅ 목표

  • 애플리케이션이 여러 서버(블루/그린)에서 동시에 실행되어도, 오직 1개의 인스턴스만 스케줄러 실행
  • Redis 분산 락 기반으로 경쟁
  • 락이 설정된 시간 동안만 리더 역할 수행
  • 락이 만료되면 다음 주기에 다른 서버가 선점 가능

기존 방식

// 특정 포트(8101, 8103)에서만 실행
if (!port.equals("8101") && !port.equals("8103")) {
	return;
}

✅ 1. Redis 기반 리더 선출 로직

@Slf4j
@Component
@RequiredArgsConstructor
public class LeaderElectionManager {

	private final RedisTemplate<String, Object> leaderRedisTemplate;
	private static final String LOCK_KEY = "backend:leader";
	private static final Duration LOCK_TTL = Duration.ofSeconds(60); // 락 유지 시간 (스케줄 간격보다 약간 짧게)

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

	@Value("${server.port}")
	private String port;

	/**
	 * Redis 락 획득 → 리더 여부 판단
	 */
	public boolean tryAcquireLeadership() {
		String instanceId = appName + "-" + port;

		Boolean isAcquired = leaderRedisTemplate.opsForValue().setIfAbsent(
			LOCK_KEY, instanceId, LOCK_TTL
		);

		if (Boolean.TRUE.equals(isAcquired)) {
			// 락 선점 성공 → 리더
			log.info("백엔드 스케줄러 작업 서버: {}", instanceId);
			return true;
		}

		// 이미 락을 보유하고 있는 인스턴스인지 확인
		String currentLeader = (String)leaderRedisTemplate.opsForValue().get(LOCK_KEY);
		return instanceId.equals(currentLeader);
	}
}

✅ 2. 스케줄러에서 리더일 때만 실행

@Component
@RequiredArgsConstructor
public class CrawlerScheduler {

    private final PostService postService;
    private final LeaderElectionManager leaderElectionManager;

	@TimedExecution("만료된 게시물을 삭제 스케줄러")
	@Scheduled(cron = "0 0 0 * * *")    // 매일 자정 마다 실행
	public void deleteExpiredPostsScheduler() {

		// 리더가 아니면 실행 X
		if (!leaderElectionManager.tryAcquireLeadership()) {
			return;
		}

		postService.deleteExpiredPosts();

	}
}

✅ 작동 방식 요약

  1. 스케줄러가 5분마다 실행됨

  2. tryAcquireLeadership()이 Redis 키 backend:leader에 락을 시도

    • 없다면 락을 선점하고 리더로 활동
    • 있다면 해당 값이 본인이면 연장된 리더
    • 본인이 아니면 실행 X
  3. TTL(60초) 뒤에 락은 자동 만료 → 다음 주기에서 다시 경쟁


✅ 왜 이 방식이 좋은가?

  • 애플리케이션 인스턴스 수에 관계없이 1개만 작업 수행
  • 락 TTL을 적절히 조절하여 작업 중단/실패에도 유연하게 대응
  • 락이 Redis에 저장되어 분산 환경에서도 정확하게 판단 가능

✅ 확장 가능성

  • 추후 ShedLock 같은 라이브러리를 써서 더 안전하게 리더 선출 및 재진입 방지 구현 가능
  • DB 기반 락도 가능하지만 Redis가 훨씬 빠르고 간단함

0개의 댓글