묠니르묘묘
꾸준히 성장하는 개발자스토리
묠니르묘묘
전체 방문자
오늘
어제
  • 분류 전체보기 (188)
    • 프로그래밍 (48)
      • 디자인패턴 (4)
      • 예외,에러 (4)
      • Java (29)
      • Kotlin (3)
      • React.js (4)
      • JavaScript (2)
      • Apache Kafka (2)
    • Spring (49)
      • Spring (21)
      • Spring Cloud (3)
      • JPA (25)
    • 코딩테스트 (31)
      • 알고리즘 (5)
      • Java - 백준 (26)
      • Java - 프로그래머스 (0)
    • AWS (7)
    • 데이터베이스 (6)
    • 개발 etc (23)
    • 도서 (5)
    • 회고록 (4)
    • 데브코스-데이터엔지니어링 (15)

인기 글

최근 글

hELLO · Designed By 정상우.
묠니르묘묘

꾸준히 성장하는 개발자스토리

선착순 쿠폰 발급을 위한 redis 분산락
Spring/Spring

선착순 쿠폰 발급을 위한 redis 분산락

2023. 5. 11. 19:03

선착순 쿠폰 발급을 위한 백엔드 시스템을 구현한 코드는 깃허브에서 볼 수 있다.

🚀 요구사항

  • 멀티 서버
  • 선착순 쿠폰 발급
  • 중복 발급 X
  • 짧은 시간 대용량 트래픽 발생

 

🚀 구현 기술스택

  • Language : Java 11
  • Framework : Spring Boot 2.7.8
  • Database : MySQL 8.0, JPA, QueryDSL, Redis
  • API Documentation : Swagger 3.0.0

 

🚀 해결 방법

1. DB Exclusive Lock(배타적 잠금)

회원쿠폰 발급 로직

  • JPA Pessimistic Lock(비관적 락)을 이용하여 DB에 배타적 잠금 사용
  • 다른 트랜잭션에서 읽기, 수정, 삭제 방지

SQL 배타적 잠금

  • SQL을 보면 select for update 구문을 사용하는 것을 볼 수 있음
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "5000")})
Optional<Coupon> findLockById(Long id);
  • 배타적 잠금은 JpaRepository에 위 코드처럼 작성하여 사용 가능
  • 다른 트랜잭션이 대기하는 시간을 @QueryHints로 설정
  • 대기시간이 지나면 LockTimeoutException 발생

 

2. Redis Distribution Lock(분산 락)

  • 배타적 잠금에서는 해당 쿠폰 레코드를 Lock 했기에, 해당 쿠폰을 사용하는 다른 서비스에서 데드락 걸릴 가능성 존재
  • 따라서 Redis를 이용한 분산락으로 변경하여 해결하였음
  • Redis Client는 대표적으로 Redisson과 Lettuce가 있음
    • Lettuce : Spin Lock을 사용함. 이는 계속 Lock을 얻으려고 시도하기에 많은 부하 발생
    • Redisson : Pub-Sub 기반으로 분산 락 제공함. 따라서 Lettuce보다 부하가 적음
📚 Pub-Sub(Publisher-Subscriber)이란?
메세지 큐 시스템으로 볼 수 있음. Pub을 구독한 Sub들에게 메세지를 알려주는 것. 따라서 여기서는 Pub이 Lock 점유를 끝냈으면 Sub에게 끝냈다고 알려주기에 Sub은 Lock을 얻으려고 시도하지 않음.

 

2-1. 코드 구현

implementation 'org.redisson:redisson-spring-boot-starter:3.17.0'
public CreateMemberCouponResponse createMemberCoupon(CreateMemberCouponCommand command) {
    RLock lock = redissonClient.getLock(couponLockName);

    try {
        if (!lock.tryLock(10, 3, TimeUnit.SECONDS)) {
            throw new RuntimeException("Lock 획득 실패");
        }
		
        // Lock 획득했으므로 아래부터는 비즈니스 로직 처리
        if (isNotStock(command.getCouponId())) {
            throw new CouponNotRemainException();
        }

        if (isDuplicateCoupon(command)) {
            throw new DuplicateCouponException();
        }

        updateCouponStatePort.decreaseRemainQuantity(command.getCouponId());
        MemberCoupon memberCoupon = createMemberCouponPort.createMemberCoupon(command.getMemberId(), command.getCouponId());
        return new CreateMemberCouponResponse(memberCoupon.getId(), command.getCouponId(), memberCoupon.getCreateDateTime());
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        if (lock != null && lock.isLocked()) {
            lock.unlock();
        }
    }
}

왼쪽 : 배타적 잠금, 오른쪽 : Redis 분산락 적용

 

기존 코드와 비교하기위해 사진도 첨부했다. Redis 분산락을 얻기 위한 getLock()과 tryLock()이 생기면서 try/catch 문이 생겨서 코드의 복잡성이 증가했다. 해당 코드는 여기에서 볼 수 있다.

 

🚀 끝내면서

  • 트래픽이 적고, 다른 서비스에서 해당 쿠폰을 많이 사용하지 않으며, 다른 라이브러리를 의존하지 않고 끝내려면 배타적 잠금을 사용
  • 트래픽이 많고, 다른 서비스에서 해당 쿠폰을 많이 사용한다면 Redis 분산락 사용

다른 방법으로는 대기열 아키텍처 방식도 있다. 대규모 동시 요청이 들어온다면 Redis 분산락도 많은 부하가 생길 수 있다. 따라서 대기열을 만들어 처리하는 방법이 있다. 이 대기열 방식은 콘서트나 티켓 예매 사이트와 아래 사이트를 참고하여 알게되었는데 나중에 한번 만들어보고 싶다.

  • 지마켓 대기열 시스템
  • 접속대기 시스템
  • 우아한형제들 선착순 이벤트 서버 생존기! 47만 RPM에서 살아남다?!
  • 프로모션을 대비한 대기열 시스템 구성하기(Redis, WebSocket, Spring)

 


참고

Redis 공식문서

저작자표시 비영리 (새창열림)

'Spring > Spring' 카테고리의 다른 글

로컬에서 동일한 SpringBoot 서버를 여러개 실행하는 방법  (0) 2023.06.02
SpringBoot 랜덤포트로 실행하기  (3) 2023.05.15
트랜잭션을 사용할 때 각 DB들의 기본 격리 수준은 무엇일까?  (0) 2023.02.20
포스트맨으로 url 요청했는데 405 에러  (0) 2023.02.13
스프링 부트 의존관계 주입 에러  (0) 2023.02.13
    'Spring/Spring' 카테고리의 다른 글
    • 로컬에서 동일한 SpringBoot 서버를 여러개 실행하는 방법
    • SpringBoot 랜덤포트로 실행하기
    • 트랜잭션을 사용할 때 각 DB들의 기본 격리 수준은 무엇일까?
    • 포스트맨으로 url 요청했는데 405 에러
    묠니르묘묘
    묠니르묘묘

    티스토리툴바