기존 블루/그린 배포 환경에서는 특정 포트(8101, 8103)에서만 스케줄러가 실행되도록 했습니다. 하지만 두 서버가 동시에 떠있으면 둘 다 작업을 수행하는 중복 문제가 있었습니다.
이를 해결하기 위해 Redis 분산 락(Distributed Lock) 패턴을 도입했습니다.
Redis 분산 락 기반으로 경쟁// 특정 포트(8101, 8103)에서만 실행
if (!port.equals("8101") && !port.equals("8103")) {
return;
}
@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);
}
}
@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();
}
}
스케줄러가 5분마다 실행됨
tryAcquireLeadership()이 Redis 키 backend:leader에 락을 시도
TTL(60초) 뒤에 락은 자동 만료 → 다음 주기에서 다시 경쟁