Redis Lock

조민재·2025년 2월 3일

키밸류 저장소인 Redis는 유명한 클라이언트 라이브러리들이 3가지 정도 있습니다.
Lettuce, Redisson, Jedis가 있는데, Jedis는 구현 방식이 간단하지만 비동기 처리를 지원하지 않으며 Thread-safe 하지 않아 잘 쓰지 않는 추세라고 합니다.

Jedis vs Lettuce

Jedis는 동기-블로킹 방식으로 동작합니다.
Lettuce는 Netty 기반으로 만들어져 비동기-논블로킹 방식을 지원합니다.

Lettuce가 Jedis보다 우수한 점들을 살펴보면:
1. 스레드 안정성: Lettuce는 thread-safe하여 여러 스레드에서 하나의 연결을 공유할 수 있습니다. 반면 Jedis는 thread-safe하지 않아 멀티스레드 환경에서는 connection pooling을 사용해야 합니다.
2. 연결 관리: Lettuce는 자동 재연결과 장애 조치를 지원합니다. Netty 기반이라 네트워크 지연이나 연결 문제에 더 강건합니다.
3. 성능: Lettuce는 비동기 작업을 지원하여 동시성 처리에서 더 나은 성능을 보입니다. 특히 대량의 요청을 처리할 때 이점이 있습니다.
4. 클러스터 지원: Lettuce는 Redis 클러스터에 대한 더 나은 지원을 제공합니다. 토폴로지 변경을 자동으로 감지하고 처리할 수 있습니다.
5. 분산 락(Distributed Lock): Lettuce는 Redis의 분산 락 구현에 있어 더 안정적이고 효율적입니다. 단순히 락 관련 기능뿐만 아니라, 전반적인 동시성 처리에서 우수합니다.

Jedis가 여전히 유용할 수 있는 경우는:
1. 단순한 사용 사례: 간단한 캐싱이나 적은 트래픽의 애플리케이션에서는 Jedis의 단순함이 장점이 될 수 있습니다.
2. 레거시 시스템: 기존 Jedis를 사용하는 시스템에서 마이그레이션 비용이 큰 경우, 현재 시스템이 안정적으로 동작한다면 굳이 변경할 필요가 없을 수 있습니다.
3. 학습 목적: Redis를 처음 배우는 개발자에게는 Jedis의 직관적인 API가 도움이 될 수 있습니다.

Lettuce가 단순히 분산 락에서만 우수한 것이 아니라, 전반적인 성능과 안정성, 기능성 측면에서 더 나은 선택입니다. 하지만 Jedis도 여전히 그 자리가 있으며, 상황에 따라 적절한 선택이 될 수 있습니다.

Lettuce vs Redission

Lettuce는 Redis의 저수준(low-level) 클라이언트로서 Redis의 기본 기능들을 직접적으로 다룰 수 있게 해주는 데 중점을 두고 있습니다.
반면 Redisson은 분산 자바 객체와 서비스를 제공하는 데 초점을 맞춘 고수준(high-level) 클라이언트입니다.

성능 측면에서 보면, 두 라이브러리 모두 Netty 기반으로 작동하여 비동기-논블로킹 방식을 지원하기 때문에 우수한 성능을 보여줍니다.
하지만 사용 목적과 방식에 따라 성능 차이가 발생할 수 있습니다.
Lettuce는 더 낮은 수준의 제어가 가능하기 때문에, 최적화된 구현을 통해 특정 상황에서 더 나은 성능을 끌어낼 수 있습니다.
분산 락(Distributed Lock)의 구현 방식에서도 중요한 차이가 있습니다. Redisson은 더 견고한 분산 락 구현을 제공합니다.
특히 Redisson은 pub/sub 메커니즘을 활용하여 락 획득 대기 시 스핀 락(spin lock)을 피하고, 워치독(watchdog) 메커니즘을 통해 클라이언트 크래시 시에도 락이 자동으로 해제되도록 보장합니다. Lettuce도 분산 락을 구현할 수 있지만, 이러한 고급 기능들은 직접 구현해야 합니다.

기능적인 측면에서 보면, Redisson은 다음과 같은 추가적인 분산 객체들을 제공합니다:

  • 분산 컬렉션 (Set, List, Map, Queue 등)
  • 분산 락과 세마포어
  • 분산 실행 서비스 (Executor Service)
  • 분산 토픽 (Pub/Sub)
  • 리모트 서비스
  • 분산 실시간 객체

이러한 차이점들을 고려할 때, 다음과 같은 상황에서 각각의 라이브러리를 선택하는 것이 좋습니다:
Lettuce를 선택하면 좋은 경우:
1. Redis의 기본 기능들을 직접적으로 다루고 싶을 때
2. 커스텀한 구현이 필요하거나 더 세밀한 제어가 필요할 때
3. 단순한 캐싱이나 데이터 저장이 주요 용도일 때
4. Spring Data Redis와 함께 사용할 때 (Spring Boot의 기본 Redis 클라이언트)

Redisson을 선택하면 좋은 경우:
1. 분산 락이나 세마포어가 중요한 요구사항일 때
2. 분산 자바 객체들을 편리하게 사용하고 싶을 때
3. 분산 컬렉션이나 서비스가 필요할 때
4. 더 높은 수준의 추상화가 필요할 때

분산 락을 구현할 때:
Lettuce를 사용하는 경우, 기본적인 락 구현은 가능하지만 추가적인 기능들은 직접 구현해야 합니다:

// Lettuce를 사용한 분산 락 구현
public boolean lock(String key) {
    return connection.set(key, "locked", SetArgs.Builder.nx().ex(30));
}

반면 Redisson은 더 견고한 락 구현을 바로 사용할 수 있습니다:

// Redisson을 사용한 분산 락 구현
RLock lock = redisson.getLock("myLock");
try {
    // 자동으로 워치독 메커니즘 적용, 락 획득 실패 시 효율적인 대기
    lock.lock(30, TimeUnit.SECONDS);
    // 비즈니스 로직
} finally {
    lock.unlock();
}

결론적으로, Lettuce와 Redisson은 서로 다른 목적과 사용 사례를 가지고 있으며, 상호 보완적인 관계라고 볼 수 있습니다. 프로젝트의 요구사항과 특성에 따라 적절한 라이브러리를 선택하거나, 때로는 두 라이브러리를 함께 사용하는 것도 좋은 선택이 될 수 있습니다.

분산 환경에서의 동시성 제어는 항상 까다로운 문제입니다. 여러 서버에서 동일한 리소스에 접근할 때 발생할 수 있는 경쟁 상태(Race Condition)를 방지하기 위해 분산 락(Distributed Lock)이 필요합니다.
Redis를 활용한 안전한 분산 락(Distributed Lock) 구현에 대해서 좀 더 알아보겠습니다.

1. 분산 락이 필요한 이유
단일 서버 환경에서는 Java의 synchronized 키워드나 ReentrantLock 같은 로컬 락을 사용하여 동시성을 제어할 수 있습니다. 하지만 여러 서버가 동작하는 분산 환경에서는 이러한 로컬 락으로는 충분하지 않습니다.
예를 들어, 동일한 주문 번호에 대한 결제 처리가 여러 서버에서 동시에 발생할 수 있습니다. 이때 분산 락이 없다면 중복 결제가 발생할 수 있죠. 이러한 문제를 해결하기 위해 분산 락이 필요합니다.

2. Redis를 이용한 분산 락 구현 방식
Redis를 사용한 분산 락은 주로 다음과 같은 방식으로 구현됩니다:
2.1 SETNX를 이용한 기본 구현
가장 기본적인 방식은 Redis의 SETNX(SET if Not eXists) 명령어를 사용하는 것입니다.

public boolean lock(String key) {
    return redisTemplate.opsForValue()
            .setIfAbsent(key, "LOCKED", Duration.ofSeconds(30));
}

public boolean unlock(String key) {
    return redisTemplate.delete(key);
}

하지만 이 방식은 다음과 같은 문제점들이 있습니다:
락 획득 실패 시 계속해서 재시도하는 방식(스핀 락)으로 인한 리소스 낭비
락을 가진 프로세스가 비정상 종료될 경우 락이 영구적으로 남을 수 있음
락 해제 시 다른 프로세스의 락을 실수로 해제할 수 있음

2.2 Lua 스크립트를 이용한 개선된 구현
위의 문제점들을 해결하기 위해 Lua 스크립트를 사용할 수 있습니다.

public class RedisLock {
    private static final String LOCK_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then" +
        "  return redis.call('del', KEYS[1]) " +
        "else" +
        "  return 0 " +
        "end";

    private final StringRedisTemplate redisTemplate;
    private final String lockKey;
    private final String lockId;

    public boolean lock() {
        return redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockId, Duration.ofSeconds(30));
    }

    public boolean unlock() {
        RedisScript<Long> script = RedisScript.of(LOCK_SCRIPT, Long.class);
        return redisTemplate.execute(script, 
                Collections.singletonList(lockKey), 
                lockId).equals(1L);
    }
}

3. 분산 락 구현 시 고려사항
3.1 락의 유효 시간 설정
락에 적절한 유효 시간(TTL)을 설정하는 것이 중요합니다. 이는 다음과 같은 이유 때문입니다:
프로세스 크래시로 인한 데드락 방지
네트워크 파티션 상황에서의 복구 가능성 확보
비즈니스 로직 실행 시간을 고려한 적절한 타임아웃 설정
3.2 재시도 메커니즘
락 획득 실패 시의 재시도 전략도 중요합니다:

public boolean lockWithRetry() {
    int retries = 3;
    long retryDelay = 1000L;
    
    while (retries > 0) {
        if (lock()) {
            return true;
        }
        retries--;
        try {
            Thread.sleep(retryDelay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    return false;
}

3.3 워치독(Watchdog) 메커니즘
긴 시간 동안 락을 유지해야 하는 경우, 워치독 메커니즘을 통해 락의 유효 시간을 자동으로 연장할 수 있습니다:

public class WatchdogRedisLock {
    private final ScheduledExecutorService scheduler = 
            Executors.newSingleThreadScheduledExecutor();
    
    public void lockWithWatchdog() {
        if (lock()) {
            // 락 유효 시간의 1/3 시점에 갱신
            scheduler.scheduleAtFixedRate(
                this::renewLock,
                10,
                10,
                TimeUnit.SECONDS
            );
        }
    }
    
    private void renewLock() {
        redisTemplate.expire(lockKey, Duration.ofSeconds(30));
    }
}

4. Redisson vs Lettuce 비교
4.1 Redisson의 장점
Redisson은 이러한 분산 락 구현을 위한 고수준의 추상화를 제공합니다:

RLock lock = redisson.getLock("myLock");
try {
    // 자동 워치독 메커니즘 적용
    lock.lock(30, TimeUnit.SECONDS);
    // 비즈니스 로직
} finally {
    lock.unlock();
}

Redisson의 특징:
pub/sub 기반의 효율적인 락 획득 대기
자동 워치독 메커니즘
재진입 가능한 락 지원
락 해제 보장

4.2 Lettuce를 사용한 구현
Lettuce는 더 낮은 수준의 제어를 제공합니다:

public class LettuceLock {
    private final RedisClient redisClient;
    
    public boolean tryLock(String key, Duration timeout) {
        try (StatefulRedisConnection<String, String> connection = 
                redisClient.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.set(key, "locked", 
                    SetArgs.Builder.nx().ex(timeout.getSeconds()));
        }
    }
}

5. 실제 구현 예제
다음은 Spring Boot에서 AOP를 활용한 분산 락 구현 예제입니다:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();
    long timeoutSeconds() default 30L;
}

@Aspect
@Component
public class DistributedLockAspect {
    private final RedissonClient redisson;
    
    @Around("@annotation(distributedLock)")
    public Object executeLocked(ProceedingJoinPoint joinPoint, 
            DistributedLock distributedLock) throws Throwable {
        String lockKey = generateKey(joinPoint, distributedLock);
        RLock lock = redisson.getLock(lockKey);
        
        try {
            boolean locked = lock.tryLock(
                    distributedLock.timeoutSeconds(), 
                    TimeUnit.SECONDS);
            if (!locked) {
                throw new LockAcquisitionException();
            }
            return joinPoint.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
@Service
public class PaymentService {
    @DistributedLock(key = "'payment:' + #orderId")
    public void processPayment(String orderId) {
        // 결제 처리 로직
    }
}

6. 장애 상황 대처 방안
6.1 네트워크 파티션 상황
네트워크 파티션이 발생했을 때를 대비하여 다음과 같은 전략을 고려해야 합니다:
Redis 클러스터 구성
적절한 타임아웃 설정
재시도 메커니즘 구현

6.2 Redis 서버 다운
Redis 서버 장애에 대비한 방안:
Redis Sentinel을 통한 고가용성 확보
백업 Redis 서버 구성
장애 상황에서의 락 획득 실패를 고려한 폴백 메커니즘 구현

분산 락은 분산 환경에서 동시성을 제어하기 위한 필수적인 요소입니다. Redis를 사용한 분산 락 구현 시에는 다음 사항들을 반드시 고려해야 합니다:
적절한 타임아웃 설정과 재시도 메커니즘
락 해제의 신뢰성 확보
장애 상황 대응 방안
성능과 안정성의 균형

profile
안녕하세요

0개의 댓글