FairLock을 활용한 순서 보장 확인

coldrice99·2024년 12월 20일
0

문제 상황

처음에는 Redis getFairLock을 사용하여 처리 순서를 보장하려 했으나, 테스트 로그를 통해 순서가 보장되지 않는다고 잘못 판단했다. 이로 인해 FairLock은 성능만 저하시킨다고 생각해 사용을 배제했다.
그러나 서비스 로그를 통해 Redis Fair Lock이 요청 순서를 정확히 보장하고 있었음을 뒤늦게 확인했다.


트러블슈팅 과정

1. 초기 테스트 로그와 문제점

초기 테스트 코드는 스레드 시작 및 완료 로그를 출력하여 처리 순서를 확인했다.

@Test
@DisplayName("Redis 락 동시성 제어 테스트")
void redisLockConcurrencyTest() throws InterruptedException {
    for (int i = 0; i < threadCount; i++) {
        int threadId = i;
        executorService.submit(() -> {
            try {
                System.out.println("Thread 시작: " + Thread.currentThread().getId());
                Member threadMember = new Member("Test User" + threadId, "test" + threadId + "@example.com",
                    "password", MemberRole.USER);
                memberRepository.save(threadMember);
                couponService.issueCoupon(coupon.getId(), new RequestDto(threadMember.getId()), threadMember);
            } catch (GlobalException e) {
                System.out.println("Thread 예외: " + Thread.currentThread().getId() + ", " + e.getMessage());
            } finally {
                latch.countDown();
                System.out.println("Thread 완료: " + Thread.currentThread().getId());
            }
        });
    }
}

테스트 로그 결과:

Thread 시작: 70
Thread 시작: 63
Thread 시작: 61
...
Thread 완료: 61
Thread 완료: 70
Thread 완료: 63
...

문제점: 이 로그를 기반으로 FairLock이 제대로 동작하지 않는다고 판단했다.
그러나 테스트 코드의 Thread 시작 로그는 실제 락 처리 순서를 반영하지 않았다.


2. 서비스 로그를 통한 검증

서비스 코드에서 실제로 락을 대기하고 획득하는 부분에 로그를 추가해 검증했다.

@Transactional
public void issueCoupon(Long couponId, RequestDto request, Member member) {
    String lockKey = "coupon:" + couponId;
    RLock lock = redissonClient.getFairLock(lockKey);

    try {
        System.out.println("대기중: " + Thread.currentThread().getId());
        if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) {
            System.out.println("Thread " + Thread.currentThread().getId() + "락 획득 실패.");
            throw new IllegalStateException("동시에 너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.");
        }
        System.out.println("Thread " + Thread.currentThread().getId() + "락 획득.");

        // 비즈니스 로직 처리
        Coupon coupon = couponRepository.findById(couponId)
            .orElseThrow(() -> new GlobalException(COUPON_NOT_FOUND));
        coupon.validateQuantity();
        coupon.decreaseQuantity();

        Issuance issuance = Issuance.builder()
            .member(member)
            .coupon(coupon)
            .build();
        issuanceRepository.save(issuance);

    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            System.out.println("Thread " + Thread.currentThread().getId() + " released lock.");
        }
    }
}

3. 로그 결과를 통한 순서 보장 확인

대기 중 로그락 획득 및 해제 로그를 비교해 FairLock이 요청 순서를 정확히 보장하고 있음을 확인했다.

  1. 대기 중 순서:

    63, 71, 65, 70, 62, 61, 66, 64, 68, 67, 103, 98, 139...
  2. 락 획득 순서:

    70, 63, 61, 65, 64, 71, 62, 66, 68, 67, 103, 98, 139...

분석:

  • 초기 몇 개의 스레드는 락 대기 순서와 락 획득 순서가 불일치했으나, 68번 스레드 이후부터는 락 대기 순서대로 락을 획득하고 있었다.
  • 초기 순서 불일치는 스레드 스케줄링의 비결정성로그 출력 시점과 락 처리의 비동기적 특성에 의해 발생한 것이다.

결론

  1. Redis Fair Lock은 요청 순서를 보장:

    • FairLock은 Redis 대기열을 기반으로 락 획득 순서를 보장한다는 점이 확인됐다.
    • 초기 혼란은 테스트 로그가 실제 순서를 정확히 반영하지 않았기 때문이었다.
  2. 초기 순서 불일치는 자연스러운 현상:

    • 스레드 스케줄링과 Redis 통신의 비동기적 특성으로 인해, 초기 몇 개의 스레드에서는 대기 순서와 락 획득 순서가 다를 수 있다.
    • 그러나 이후부터는 락 대기열에 따라 순서가 명확히 유지된다.
  3. FairLock 사용 유지 결정:

    • FairLock을 사용하면 성능이 약간 저하될 수 있지만, 요청 순서가 중요한 경우에는 반드시 사용해야 한다는 결론을 내렸다.
profile
서두르지 않으나 쉬지 않고

0개의 댓글