【흑마 품평-3초 특가 쿠폰】RabbitMQ + Lua를 활용한 분산 처리

Redis 기반 분산 락의 한계와 해결 방안

  • 재진입 불가 문제: 동일 스레드가 같은 락을 두 번 이상 획득할 수 없음. 예를 들어 메서드 A에서 메서드 B를 호출하고, 둘 다 락을 필요로 한다면, 첫 번째 호출에서 락을 획득한 후 두 번째 호출 시 락을 재획득하지 못해 데드락 발생 가능.
  • 재시도 기능 부족: 락 획득 시도는 단 한 번만 수행되고 실패 시 즉시 반환되며, 재시도 메커니즘이 없어 데이터 손실 위험이 있음. 예컨대 스레드 1이 락을 확보하고 데이터베이스에 기록하려는 도중 다른 스레드가 락을 차지하면, 스레드 1은 실패 후 종료되어 작업이 사라짐.
  • 타임아웃 자동 해제의 리스크: 타임아웃 설정은 데드락을 줄이는 데 도움이 되지만, 처리 시간이 길어질 경우 락이 조기에 해제되어 보안 취약점이 생김. 너무 짧으면 비정상 종료 시 락이 해제되며, 너무 길면 데드락 발생 가능성 증가. 이는 균형 잡힌 해결책이 필요함. 해결법으로는 짧은 타임아웃과 함께 헬스체크 및 자동 연장 기술을 적용할 수 있음.
  • 마스터-슬레이브 일관성 문제: Redis 클러스터 환경에서는 마스터와 슬레이브 간 복제 지연이 발생할 수 있어, 일부 노드에서 락 상태가 일치하지 않을 수 있음.

해결책: Redisson 프레임워크 활용

Redisson는 고도로 발전된 Redis 클라이언트 라이브러리로, 분산 락, 동시성 제어, 분산 컬렉션, 분산 객체 등 다양한 분산 시나리오에 대한 완전한 솔루션 제공.

핵심 로직 구현

  1. 요청 제한: Guava RateLimiter 사용
    매 초 최대 10건의 요청만 허용하도록 설정. 1초 내에 토큰을 획득하지 못하면 실패 처리.
    private final RateLimiter rateLimiter = RateLimiter.create(10);
    if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
        return Result.fail("현재 네트워크가 혼잡합니다. 다시 시도해 주세요");
    }
  2. 사용자 식별: ThreadLocal 기반 세션 관리
    현재 사용자의 식별자 추출.
    Long userId = UserHolder.getUser().getId();
  3. Lua 스크립트 실행
    Redis에 저장된 `seckill.lua` 스크립트를 원자적으로 실행하여 동시성 문제를 방지.
    1. 스크립트 인자: 쿠폰 ID, 사용자 ID
    2. 인자 전달: 빈 리스트로 매개변수 대체
    3. 실행 명령:
      Long result = stringRedisTemplate.execute(
          SECKILL_SCRIPT,
          Collections.emptyList(),
          voucherId.toString(),
          userId.toString()
      );
  4. Lua 스크립트 내용 (seckill.lua)
    -- 입력 파라미터
    local voucherId = ARGV[1]        -- 쿠폰 ID
    local userId = ARGV[2]           -- 사용자 ID
    
    -- Redis 키 정의
    local stockKey = 'seckill:stock:' .. voucherId     -- 재고 키
    local orderKey = 'seckill:order:' .. voucherId     -- 주문 기록 키
    
    -- 재고 확인
    if tonumber(redis.call('get', stockKey)) <= 0 then
        return 1  -- 재고 부족
    end
    
    -- 중복 주문 여부 확인
    if redis.call('sismember', orderKey, userId) == 1 then
        return 2  -- 이미 주문한 사용자
    end
    
    -- 재고 감소
    redis.call('incrby', stockKey, -1)
    
    -- 사용자 추가 (주문 기록)
    redis.call('sadd', orderKey, userId)
    
    return 0  -- 성공
  5. 주문 생성 및 큐 전송
    Lua 스크립트 결과가 0(성공)일 경우, 주문 정보 생성 후 RabbitMQ로 비동기 전송.
    long orderId = redisIdWorker.nextId("order");
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);
    
    mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
    return Result.ok(orderId);

종합 요약

Redis 분산 락의 핵심 원칙: - `SET key value NX EX` 명령을 통해 락 획득 및 만료 시간 설정 - 락 해제 시, 소유자 식별자 검증 후 삭제 특징: - `NX` 옵션으로 상호 배타성 보장 - `EX` 옵션으로 장애 시 락 자동 해제, 데드락 방지 - 클러스터링 지원으로 고가용성 및 고성능 달성

태그: RabbitMQ Lua Redis 분산 락 스크립트 원자성

6월 23일 01:11에 게시됨