[Redis] Java Redis Client의 분산락 구현 방식

이재민·2024년 5월 6일

Redis

목록 보기
5/6

선착순 쿠폰 발급 이벤트

작년 가을쯤 현재 재직중인 회사에서 선착순 쿠폰을 발급하는 이벤트 개발을 맡게되었습니다.
선착순 쿠폰 발급 이벤트를 개발하면서 학습한 내용을 기록하고자 합니다.

선착순 쿠폰 발급 이벤트 요구사항

  1. 특정 기간에 다운 가능한 쿠폰이 존재하며, 총지급 수량은 한정되어 있다.
  2. 쿠폰 수량은 정해진 양을 초과해서는 안된다.
  3. 쿠폰은 1인당 1장만 발급가능하다.
이번 선착순 쿠폰 발급은 게릴라 세일 구좌를 차지한 셀러들의 쿠폰을 특정 시간이 도래하면 다운 받을 수 있습니다. 이때, 다수의 요청이 발생하게 된다면 정해진 수량의 쿠폰보다 많은 양의 쿠폰이 발급될 수 있다.

위 요구사항을 만족하기위해 분산락을 통해 동시성 이슈를 해결하고 Redis를 활용하기로 하였다.

동시성을 해결하기 위한 방안으로는 여러가지가 있습니다.

  • Java Synchronized 사용
  • DB 제어
    • Pessimistic Lock(비관적 락) 활용
    • Optimistic Lock(낙관적 락) 활용
    • Named Lock 활용
  • Redis 활용
    • Redis Lettuce Client 활용
    • Redis Redission Client 활용

분산락

분산락이란, 여러 대의 서버나 프로세스 간에 데이터 또는 자원을 안전하게 동기화하기 위한 매커니즘이다.
DB 등 공통된 저장소를 이용하여 자원이 사용 중인지를 체크하기에 전체 서버에서 동기화된 처리가 가능해진다.

이번 개발의 목표는 쿠폰 발급의 동시성 문제뿐만 아니라 다양한 기획에서도 사용할 수 있도록 공통화된 분산락을 구현하는 것이 목표였다.
분산락을 구현한 덕분에 블로그 포스팅 날짜 기준 서비스 내 여러 기획에서도 분산락을 문제 없이 사용중에 있다.

자바 Redis 클라이언트가 분산락을 구현한 방식에 대해 알아보자

Jedis, Lettuce, Redisson은 모두 자바 레디스 클라이언트이다.
Jedis는 동기식 blocking I/O를 사용한다.
Lettuce, Redisson은 는 비동기 non-blocking I/O를 사용한다.
그래서 Jedis를 제외한 Lettuce, Redisson은 분산락을 어떻게 구현했는지 알아보도록 하자

위에서도 언급했듯이 동시성을 해결하기 위한 방안은 여러가지가 있으며 그에 따른 장/단점이 존재한다.
Redis의 Jedis는 Lettus에 비해 압도적으로 성능차이가 발생한다.
자세한 내용은 이동욱님 블로그를 참고하세요.

Lettuce

  • 1. Lettuce는 스핀 락 기반으로 동작한다.
    • 그래서 클라이언트로부터 요청이 많을수록 부하는 커지고 retry 로직을 개발자가 직접 구현해야 한다.
  • 2. 타임아웃이 지정되어있지 않다.
    • 스핀 락은 락을 획득하지못하면 무한 루프를 돌게됩니다.
      운좋게 tryLock을 성공하였더라도 애플리케이션 이슈가 발생하면 다른 애플리케이션은 무한정 대기상태가 되어버린다.
      그렇기 때문에 일정 시간이 지나면 락이 만료되도록 구현해야 한다. 하지만, automic한 명령어 수행을 위해 setnx 로 묶여있기에 expire time을 지정할 수 없기에 문제 해결하기가 어렵다.
  • 3. Redis에 많은 부하를 준다.
    • 스핀락을 사용하게 된다면 Redis에 엄청난 부담을 주게 된다. 락 획득을 위해 지속적으로 요청을 보내게 되고 Redis는 트래픽 처리로 부담을 받게 된다.
    • 예를 들어 300ms가 걸리는 동기화 작업에 동시에 100개의 요청이 들어왔다고 가정하겠습니다.
      처음 락을 획득하는데 성공한 1개의 요청을 제외한 나머지 99개의 요청은 작업이 완료되는 300ms 동안 무려 Redis에 594회의 락 획득 요청을 보내게 된다. 즉, 1초에 2000회라는 요청을 Redis에 보내게 된다.
      매 50ms마다 락 시도. 락 획득에는 300ms. (300ms/50ms) 총 6회 시도. 첫 번째 요청을 제외한 5번 시도를 99개의 클라이언트로부터 요청이 들어옴. 99*5 = 495회

Redisson

  • 스핀 락을 사용하지 않는다.
    • 스핀 락을 사용하지 않고 락의 획득 가능 여부를 판단하기 위해 pub/sub 기능을 사용하여 스핀 락의 엄청난 트래픽을 줄였습니다.
    • 락이 해제될 때마다 subscribe하는 클라이언들에게 알림을 주어서 스핀락의 부담을 개선하였다.
    • 이런 방식으로 락 획득을 기다리는 클라이언트는 타임아웃(아래 내용에서 설명)이 지나기 전까지 락 해제 메시지를 기다리게 됩니다.
  • 타임아웃이 구현되어 있다.
    • Redission의 tryLock 메소드에 타임아웃을 명시하도록 되어있다.
      첫 번째 파라미터는 락 획득을 대기할 타임아웃(위에서 언급한 타임아웃), 두 번째 파라미터는 락이 만료되는 시간이다.
      public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
      	
  • Lua 스크립트를 사용한다.
    • Redis는 싱글스레드로 명령을 수행하기 때문에 원자적(automic)이다. 하지만, 각 명령어를 따로 보내게 되면 두 연산이 atomic 하지 않게 수행되기에 예상과 다른 결과가 나올 수 있다.
    • 그렇기 때문에 락에서 사용되는 여러 연산은 atomic해야 하기에 Redisson은 Lua 스크립트 사용을 지원한다.
      1. 락 획득 가능 여부 확인, 락 획득은 atomic해야 한다.
        두 명령이 atomic하지 않으면 명령어가 수행되는 사이에 다른 스레드에서 락을 획득할 수 있기 때문이다.
      2. pub/sub 두 명령은 atomic해야 한다.
        락이 해제되고 바로 다른 스레드에서 락을 획득했을 때도 subscribe중인 클라이언트에게 락 획득을 시도하라는 메시지가 발행되기 때문이다.
    • Redis는 트랜잭션, Lua 스크립트로 atomic한 연산을 지원하지만, 트랜잭션은 명령어를 트랜잭션으로 묶는 기능이기에 결과를 받아 다른 연산에 활용하는 atomic한 연산을 구현하기는 어렵다. 반면 Lua 스크립트는 쉽게 구현할 수 있다.

참고
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
https://jojoldu.tistory.com/418

profile
문제 해결과 개선 과제를 수행하며 성장을 추구하는 것을 좋아합니다.

0개의 댓글