본문 바로가기

Backend/infra

Redisson vs Lettuce: 분산 환경에서의 정합성

1. Lettuce의 락 동작 방식: 스핀 락 (Spin Lock)

Lettuce는 Redis의 SETNX (Set if Not eXists) 명령어를 사용하여 분산 락을 구현한다.

 

동작 원리: 락 획득 실패 시 일정 시간 대기 후 다시 재시도하는 루프를 돈다.

  • 시도: 애플리케이션이 Redis에 SETNX 명령어를 통해 특정 키를 저장하도록 한다. 저장에 성공하면 락을 획득한 것으로 간주한다.
  • 실패 시 재시도 (Polling): 만약 이미 키가 존재하여 락 획득에 실패하면 애플리케이션은 일정 시간 동안 대기한 후 다시 SETNX 명령을 보낸다.
  • 무한 루프: 락을 획득할 때까지 혹은 타임아웃이 발생할 때까지 이 과정을 반복한다.

특징과 단점

  • Redis 부하: 락을 대기 중인 스레드가 많을수록 Redis 서버에 요청을 더 많이 보내게 되므로 Redis의 CPU 사용량이 급증한다.
  • 지연 시간: 재시도 간격이 존재하므로 실제 락이 해제된 시점과 다음 재시도 시점 사이의 간극만큼 응답 시간이 지연된다. ⇒ 예를 들어, 재시도를 1초 간격으로 진행할 때 락이 2.5초 시점에 해제되었음에도 3초 시점에 해제된 것을 알게 됨.

Redis Cluster 환경에서의 문제점

  • 비동기 복제로 인한 락 유실: Lettuce는 먼저 Master 노드에 락을 기록한다. 만약 Master 노드에 락을 기록 후 Replica 노드에 기록하기 전에 Master 노드에 장애가 발생한다면 failover 후 새로운 Master 노드에는 락 정보가 없다. ⇒ 동일한 자원에 대해 다른 스레드가 락을 획득할 수 있게 됨 (중복 락)

2. Redisson과 Redlock 알고리즘: 고가용성 보장

Redisson은 Redis의 Pub/Sub 사용하여 락을 제어할 뿐만 아니라, 다중 노드 환경을 위한 Redlock 알고리즘을 구현체로 사용한다.

 

Redlock 알고리즘이란:

  • 서로 독립적인 N개의 Standalone Redis 인스턴스(Cluster 구조 X) 중에서 절반 이상이 락을 획득해야 최종적으로 락을 획득한 것으로 간주한다. 이는 단일 클러스터의 샤딩 구조나 비동기 복제 문제를 해결하여, 일부 노드나 가용 영역(AZ) 전체에 장애가 발생하더라도 데이터 정합성을 유지하기 위함이다

동작 원리

  1. 시도: 락 획득을 시도한다.
  2. Subscribe: 락 획득에 실패하면 해당 채널을 구독하고 대기 상태가 된다. 이때, Lettuce 구현체와 달리 CPU 자원을 거의 사용하지 않는다.
  3. Publish: 락을 점유하고 있던 스레드가 Publish를 통해 락을 해제한다.
  4. 재시도: 대기 중이던 스레드들은 Subscribe를 통해 락을 획득한다.

특징과 장점

  • CPU 부하 방지: Spin Lock 방식이 아니므로 Redis 서버와 애플리케이션 서버 둘 다 CPU 부하가 낮다.
  • 정합성과 편의성: 락 만료 시간을 자동으로 연장해주는 Watchdog 기능과 재진입 락(Reentrant Lock) 기능을 기본으로 제공한다.

3. 차이점 비교

항목 Lettuce (Spin Lock) Redisson (Pub/Sub)
재시도 방식 주기적으로 계속 요청 (Polling) 신호가 올 때까지 대기 (Subscribe)
Redis 부하 대기 스레드가 많을수록 높음 상대적으로 낮음
구현 난이도 직접 재시도 로직을 구현해야 함 내장된 API(tryLock)로 간편히 사용
타임아웃 처리 직접 로직으로 구현 필요 기본 파라미터로 제공

4. 코드 예시 비교

Lettuce (직접 재시도 로직 구현 필요)

public void registerUser(String email) {
    String lockKey = "lock:" + email;
    // 락 획득 시도 (SETNX)
    while (!redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofSeconds(10))) {
        try {
            // 락을 얻지 못하면 100ms 대기 후 다시 루프를 돈다 (스핀 락)
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    try {
        // 비즈니스 로직 수행
    } finally {
        // 락 해제
        redisTemplate.delete(lockKey);
    }
}

Redisson (Redis Pub/Sub 사용)

public void registerWithRedisson(String email) {
    RLock lock = redissonClient.getLock("lock:" + email);

    try {
        // 최대 5초간 대기하며 신호를 기다리고, 락 획득 시 로직 수행
        // leaseTime을 -1로 설정 시 Watchdog이 작동하여 정합성 보장
        boolean isLocked = lock.tryLock(5, -1, TimeUnit.SECONDS);

        if (isLocked) {
            // 비즈니스 로직 수행 (트랜잭션 시작)
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            // 락 해제 시 Redis가 대기 스레드에게 Publish 신호를 보냄
            lock.unlock();
        }
    }
}

5. Redis Cluster + Redisson (Single Lock)

Redis Replication

 

Redisson은 락을 기록할 때 Master 노드 한 곳에만 기록하고 비동기 방식으로 Replica 노드에 복제한다.

  • 기록 순서: Master 노드에 기록 → 클라이언트에게 응답(Success) → (비동기) Replica 노드에 복제
  • 장애 발생 시: Master 노드에 락을 기록한 후 Replica 노드에 복제되기 전에 Master 노드에 장애가 발생하여 failover 발생 시 락 유실이 발생한다. ⇒ Redis Cluster의 가용성(failover에 의해)은 보장되지만 일관성은 보장하지 못함(락 유실에 의해).

6. Redlock 알고리즘을 사용한 경우

5번처럼 Replica 노드에 복제하기 전에 Master 노드에 장애가 발생하여 일관성을 보장하지 못하는 문제를 해결하기 위해 Redisson은 RedissonRedLock 객체를 제공한다.

 

Redlock은 하나의 Redis Cluster 내부의 노드들이 아니라, 물리적으로 완전히 분리된 Redis 인스턴스들에게 락을 순차적으로 저장한다. Redis Cluster는 특정 키를 해당 슬롯을 가진 단 하나의 Master 노드에게만 보내기 때문에 lock을 기록했을 때 클러스터 내부의 노드들 중에서 1대의 Master 노드에만 기록이 된다. ⇒ 나머지 Master 노드들에게 순차적으로 기록하는 것이 불가능함.

 

따라서 AWS ElastiCache를 사용하는 경우 Cluster 환경에서 Redlock 알고리즘을 사용할 수 없다. 대안으로는 Cluster 모드를 사용하지 않고, 서로 다른 AZ에 Standalone 인스턴스를 3개 또는 5개를 만들고 각 엔드포인트를 Redisson 설정에 등록해야 한다. 또 다른 방식으로는 Cluster를 사용하되 Redisson의 Single Lock 기능을 사용하고, 락 유실 방지는 DB 트랜잭션과 Optimistic Lock으로 방어해야 한다.