Redlock 알고리즘

개발자 팀·2026년 1월 26일

self-study-series

목록 보기
10/16

Redlock은 Redis의 창시자인 Salvatore Sanfilippo가 제안한 분산락 알고리즘입니다.
단일 Redis 인스턴스 환경에서도 장애시 안전하게 동작하도록 설계되어있습니다.

왜 Redlock이 필요한가?

그럼 왜 Redlock이 필요한걸까요? 일반적인 Redis 분산락(단일 인스턴스)은 단일 장애점(Single Point of Failure) 문제가 있습니다.

Redlock의 핵심은 과반수(majority) 합의 이며, 분산 시스템의 핵심을 Redis에 적용하게 됩니다.
즉 아래와 같은 상황에서 과반수 합의가 적용되는 것이죠.

✅ 5개 중 3개 이상이 동의하면 → 락 획득 성공
❌ 3개 미만만 동의하면 → 락 획득 실패

왜 과반수인가?
→ 두 클라이언트가 동시에 락을 획득하려 해도,
  과반수를 차지할 수 있는 것은 단 하나뿐이기 때문입니다.

왜 여러 Redis 인스턴스가 필요한가?

단일 Redis를 사용하게되면 그 Redis가 장애를 일으켰을 때 분산락 시슽메 전체가 마비됩니다.
이러한 상황을 미연에 방지하기 위하여 Redlock의 과반수 합의 방식이 사용되게 됩니다.
Redlock은 여러 개의 독립적인 Redis 인스턴스를 사용해 일부가 장애를 일으켜도 락 시스템이 계속 동작하도록 보장합니다.
여기서 독립적이란 복제가 아닌 완전 별개의 Redis를 의미합니다. Master-Slave 구조가 아닌 각각 독립된 Redis가 함께 운영되는 것을 말하죠.

Redlock의 3가지 핵심 원리

Redlock에는 3가지 핵심 원리가 존재합니다.
1. 다중 인스턴스 : Redis를 단일이 아닌 다중으로 관리합니다.
2. 과반수 규칙 : 다중 Redis 중 과반 이상의 락을 획득해야 분산락으로 인정됩니다.
3. 시간 검증 : 락 획득에 걸린 시간을 고려해 실제 유효 시간을 계산합니다.

이제 각 원리에 대해 자세히 살펴보도록 하겠습니다.

원리 1: 다중 Redis 인스턴스
왜 5개인가?

- 홀수 개를 권장 (3, 5, 7개)
- 5개일 때: 2개까지 장애 허용 (5 - 3 = 2)
- 3개일 때: 1개까지 장애 허용 (3 - 2 = 1)
- 7개일 때: 3개까지 장애 허용 (7 - 4 = 3)

일반적으로 5개가 가장 많이 사용됩니다.
→ 적절한 장애 허용 + 적절한 운영 비용
원리 2: 과반수 규칙 (Quorum)

쉬운 비유: 5명의 판사 중 3명 이상이 "유죄"라고 해야 유죄 판결이 나는 것과 같습니다.

과반수 공식: N/2 + 1

- 5개 인스턴스: 5/2 + 1 = 2.5 + 1 = 3개 이상 필요
- 3개 인스턴스: 3/2 + 1 = 1.5 + 1 = 2개 이상 필요

왜 과반수가 안전한가?
→ 두 클라이언트가 동시에 락을 요청해도, 5개 중 과반수(3개)를 동시에 획득할 수 있는 것은 오직 하나뿐입니다.

예시:
- 클라이언트 A: Redis 1, 2, 3에서 성공 (3개) ✅ 락 획득!
- 클라이언트 B: Redis 4, 5에서만 성공 (2개) ❌ 락 획득 실패
원리 3: 시간 검증

락을 획득하는 데 걸린 시간을 고려해야 실제로 락을 사용할 수 있는 시간을 정확히 알 수 있습니다.

예시 시나리오:

1. 락 TTL을 30초로 설정
2. 5개 Redis에 순차적으로 요청
3. 네트워크 지연 등으로 총 5초 소요

→ 실제 사용 가능 시간 = 30초 - 5초 = 25초

만약 시간 검증을 하지 않으면?
→ 클라이언트는 "30초 동안 락을 가지고 있다"고 착각
→ 실제로는 25초 후에 락이 만료되어 다른 클라이언트가 침입 가능!
특성단일 RedisRedlock (5개 인스턴스)
장애 허용❌ Redis 1대 장애 = 전체 마비✅ 2대까지 장애 허용
네트워크 파티션❌ 취약 (연결 끊기면 락 불가)✅ 과반수 연결 가능하면 동작
구현 복잡도간단 (기본 Redisson 사용)복잡 (여러 인스턴스 관리)
운영 비용낮음 (1대 운영)높음 (5대 독립 운영)
데이터 일관성단일 소스이므로 일관적시간 동기화 필요
적합한 상황일반적인 서비스금융, 결제 등 고가용성 필수

동작 과정

Redlock 알고리즘은 그럼 어떤 과정을 통해 동작할까요? 도식화하여 동작 과정을 단계별로 상세히 살펴보겠습니다.

단계별로 정리를 한 번 해보겠습니다.
1. 현재 시간을 t1에 저장합니다.
2. 모든 Redis에게 SET NX EX를 전송하여 동시에 락 요청을 합니다.
3. 각 Redis로부터의 응답을 확인합니다.
4. 과반수 이상의 Redis로부터 락을 획득했고 TTL도 0이상이면 성공한 것으로 판단합니다.
5. 이후 비즈니스 로직을 수행합니다.
6. 모든 비즈니스가 완료된 이후 모든 Redis에 DEL 명령어를 전송합니다. 이는 락 획득에 실패했을 때도 동일하게 발생합니다.

순차적으로 한 번 위 내용을 분석해보겠습니다.

Step 1: 시작 시간 기록
// 왜 시작 시간을 기록할까요?
// 그 이유는 락 획득에 걸린 시간을 계산하기 위해서입니다.
// 락 획득이 오래 걸리면, 실제 사용 가능 시간이 줄어듭니다.

long startTime = System.currentTimeMillis();
Step 2: 모든 Redis에 락 요청
// SET lock "uuid" NX EX 30 명령의 의미:
// - SET lock "uuid": lock이라는 키에 uuid 값을 저장
// - NX: 키가 존재하지 않을 때만 설정 (Not eXists)
// - EX 30: 30초 후 자동 만료

// 왜 UUID를 사용할까요?
// 락을 설정한 클라이언트만 해제할 수 있도록 하기 위해서입니다.
// 다른 클라이언트가 실수로 내 락을 해제하는 것을 방지합니다.

String lockValue = UUID.randomUUID().toString();

중요: 각 Redis에 요청하는 시간제한(timeout)을 짧게 설정해야 합니다.

  • 권장: TTL의 1/10 정도 (TTL이 30초면, 타임아웃은 3초)
  • 이유: 느린 Redis 하나 때문에 전체 락 획득이 지연되면 안 됩니다.
Step 3: 결과 수집

각 Redis의 응답을 확인합니다:

Redis 1: ✅ OK (락 획득 성공)
Redis 2: ✅ OK (락 획득 성공)
Redis 3: ❌ nil (이미 다른 클라이언트가 락 보유)
Redis 4: ✅ OK (락 획득 성공)
Redis 5: ✅ OK (락 획득 성공)

→ 총 4개 성공
Step 4: 성공 여부 판정 (핵심!)

락 획득 성공 조건 2가지를 모두 만족해야 합니다:

조건 1: 과반수 이상에서 락 획득
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
성공 개수(4) >= 과반수(3) → ✅ 만족!

조건 2: 유효 시간이 양수
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
유효 시간 = TTL - 소요시간 - 시계오차보정
         = 30초 - 0.5초 - 0.3초
         = 29.2초 > 0 → ✅ 만족!

두 조건 모두 만족 → 🎉 락 획득 성공!

만약 실패하면?

시나리오 A: 과반수 미달
- 성공 2개 < 과반수 3개 → ❌ 실패
- 즉시 모든 Redis에 DEL 명령으로 정리

시나리오 B: 시간 초과
- 락 획득에 25초 소요
- 유효 시간 = 30 - 25 - 0.3 = 4.7초
- 비즈니스 로직에 5초 이상 필요하면?
- → ❌ 실패로 처리하고 재시도
Step 5: 비즈니스 로직 실행
// 락을 성공적으로 획득했다면, 계산된 유효 시간 내에
// 비즈니스 로직을 완료해야 합니다.

// validTime = 29.2초
// 비즈니스 로직은 반드시 29.2초 내에 완료되어야 합니다!

try {
    // 비즈니스 로직 실행
    processPayment(userId, amount);
} finally {
    // 락 해제는 항상 실행
    unlock();
}
Step 6: 락 해제

핵심 규칙: 성공이든 실패든, 모든 Redis에 락 해제를 시도합니다.

// 왜 모든 Redis에 해제 요청을 보내나요?

// 1. 성공한 경우:
//    - 실제로 락을 설정한 Redis들에서 해제 필요

// 2. 실패한 경우에도 해제하는 이유:
//    - 일부 Redis에는 락이 설정되었을 수 있음
//    - 해제하지 않으면 TTL까지 불필요하게 락이 유지됨
//    - 다음 클라이언트의 락 획득을 방해함

// 3. 해제 시 UUID 확인:
//    - 자신이 설정한 락만 해제하도록 UUID 비교
//    - Lua 스크립트로 원자적으로 확인 + 삭제

String luaScript = """
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    """;

위 내용들을 한 번 도식화하여 보기 쉽게 나열해보겠습니다.

Redisson에서의 Redlock 사용

Redisson은 Redlock 알고리즘을 RedissonRedLock 클래스로 제공하여 보다 쉽게 사용할 수 있는 방법을 지원하고 있습니다.
이를 통해 직접 알고리즘 구현 없이 간단하게 사용할 수 있습니다.
글머 왜 Redisson을 함께 사용해야 할까요?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Redlock 직접 구현 시 해야 할 것들:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 여러 Redis에 동시 요청 (멀티스레딩)
2. 각 요청에 타임아웃 설정
3. 성공 개수 카운팅
4. 유효 시간 계산
5. 시계 드리프트 보정
6. 실패 시 정리(cleanup) 로직
7. 재시도 로직

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Redisson 사용 시:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
→ 이 모든 것이 RedissonRedLock 내부에 구현되어 있음
→ tryLock() 한 번 호출로 끝!

위와 같은 이유들로 Redisson을 통해 Redlock을 구현하면 쉽게 사용할 수 있게 됩니다.
그럼 어떻게 사용하는지 코드를 보면서 확인해볼까요?

Step 1: 여러 RedissonClient 설정

/**
 * Redlock 설정 클래스
 *
 * 핵심 포인트:
 * 1. 각 RedissonClient는 서로 다른 Redis 인스턴스에 연결
 * 2. 각 Redis는 완전히 독립적 (복제 관계 없음)
 * 3. 3개 또는 5개를 권장 (홀수 개)
 */
@Configuration
public class RedlockConfig {

    /**
     * Redis 인스턴스 1에 대한 클라이언트
     * - 물리적으로 다른 서버/컨테이너에 배치 권장
     * - 장애 도메인 분리가 중요
     */
    @Bean
    public RedissonClient redissonClient1() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://redis1:6379")
            .setConnectionMinimumIdleSize(5)      // 최소 유휴 커넥션
            .setConnectionPoolSize(10)             // 커넥션 풀 크기
            .setTimeout(3000)                      // 타임아웃 3초
            .setRetryAttempts(3);                  // 재시도 횟수
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient2() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://redis2:6379")
            .setConnectionMinimumIdleSize(5)
            .setConnectionPoolSize(10)
            .setTimeout(3000)
            .setRetryAttempts(3);
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient3() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://redis3:6379")
            .setConnectionMinimumIdleSize(5)
            .setConnectionPoolSize(10)
            .setTimeout(3000)
            .setRetryAttempts(3);
        return Redisson.create(config);
    }
}

Step 2: RedissonRedLock 사용

@Service
@RequiredArgsConstructor
@Slf4j
public class RedlockService {

    private final RedissonClient redissonClient1;
    private final RedissonClient redissonClient2;
    private final RedissonClient redissonClient3;

    /**
     * Redlock을 사용한 안전한 비즈니스 로직 실행
     *
     * @param userId 대상 사용자 ID
     */
    public void processWithRedlock(Long userId) {
        // 1. 락 키 정의 (모든 Redis에서 동일한 키 사용)
        String lockKey = "user:" + userId + ":lock";

        // 2. 각 Redis에 대한 RLock 객체 생성
        RLock lock1 = redissonClient1.getLock(lockKey);
        RLock lock2 = redissonClient2.getLock(lockKey);
        RLock lock3 = redissonClient3.getLock(lockKey);

        // 3. RedissonRedLock 생성 (Redlock 알고리즘 적용)
        //    내부적으로 과반수 규칙, 유효시간 검증 등을 처리
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

        try {
            // 4. 락 획득 시도
            //    - waitTime: 5초 동안 락 획득 대기
            //    - leaseTime: 30초 후 자동 해제
            boolean acquired = redLock.tryLock(5, 30, TimeUnit.SECONDS);

            if (acquired) {
                log.info("Redlock 획득 성공: userId={}", userId);

                // 5. 비즈니스 로직 실행
                //    주의: 반드시 leaseTime(30초) 내에 완료해야 함!
                doSomething();

            } else {
                // 과반수 획득 실패 또는 대기 시간 초과
                log.warn("Redlock 획득 실패: userId={}", userId);
                throw new LockAcquisitionException("락을 획득할 수 없습니다.");
            }

        } catch (InterruptedException e) {
            // 대기 중 인터럽트 발생
            Thread.currentThread().interrupt();
            throw new RuntimeException("락 획득 중 인터럽트 발생", e);

        } finally {
            // 6. 락 해제 (성공/실패 무관하게 항상 실행)
            //    내부적으로 모든 Redis에 해제 요청을 보냄
            if (redLock.isHeldByCurrentThread()) {
                redLock.unlock();
                log.info("Redlock 해제 완료: userId={}", userId);
            }
        }
    }
}

내부 동작 원리는 아래와 같습니다.


마치며.

이번 게시글에는 Redlock 알고리즘을 활용한 분산락 구현 방법을 알아보았습니다.
읽어주셔서 감사드립니다 🫡

profile
공부하고 기록하고 공유하는 개발자 팀(Tim) 입니다. 늘끄적입니다.

0개의 댓글