SPRING

[SPRING] Lettuce 분산 락 구현

도원좀비 2025. 5. 27. 18:42

1️⃣ Redis + Lettuce 분산 락 구현 방식 요약

Lettuce를 활용한 SETNX 기반의 분산 락 구조

간단하면서도 안정적인 구조로, Redis를 통해 멀티 인스턴스 환경에서도 예매 중복을 제어 가능

✅ 락 키 구성

  • lock:seat:{seatId} 형식 (예: lock:seat:1001)
  • 좌석 단위로 분산 락을 설정합니다.

✅ 락 소유자 식별

  • 락 값을 UUID로 설정하여, 락 소유자를 명확히 구분합니다.
  • 락 해제 시 자신이 소유한 락만 해제되도록 보장합니다.

✅ 락 만료시간

  • 기본 5초(px=5000)로 설정하여 데드락을 방지합니다.

전체 구조

[BookingController]
        ↓
[BookingFacade] ← 분산 락 시도
        ↓
[LettuceDistributedLockService]
        ↓
[BookingService] ← 트랜잭션

 

  • 분산 락과 트랜잭션을 명확히 분리
  • 락 책임은 Facade 계층, 트랜잭션은 Service

 


2️⃣ 락 로직 핵심 클래스

더보기

LettuceLockManager: 단일 키 락 획득/해제

public boolean tryLock(String key, String value, long expireMillis) {
    return "OK".equals(redisCommands.set(key, value, SetArgs.Builder.nx().px(expireMillis)));
}

public void unlock(String key, String value) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    redisCommands.eval(script, ScriptOutputType.INTEGER, new String[]{key}, value);
}

LettuceMultiLock: 여러 좌석 동시 락 제어

public boolean tryLockAll() {
    for (String key : keys) {
        if (!lockManager.tryLock(key, lockValue, expireMillis)) {
            unlockAll(); return false;
        }
    }
    return true;
}

LettuceDistributedLockService: 전체 흐름 제어

public void executeWithLock(List<Long> seatIds, Runnable task) {
    List<String> keys = seatIds.stream()
        .sorted()
        .map(id -> "lock:seat:" + id)
        .toList();

    LettuceMultiLock lock = new LettuceMultiLock(keys, 5000L, lockManager);
    if (!lock.tryLockAll()) throw new BookingException(LOCK_ACQUIRE_FAIL);

    try {
        task.run();
    } finally {
        lock.unlockAll();
    }
}

 


 

3️⃣ 테스트 결과 및 정리

100명 이상이 동일 좌석 A-1 예매 시도 (JMeter 사용)
동시 요청 수 성공  실패 비고
100 1 99 정상 작동
200 2~3 197~198 중복 발생 가능성 있음

중복 성공은 트랜잭션 시작 전 락과의 시간 차이 때문

해결 방법은 다음 포스트에서 트러블슈팅으로 정리 예정