본문 바로가기
Development/Spring

redis 설치 및 redisson을 이용한 분산락 구현

by 메정 2021. 10. 9.

설치

 

redis 설치

위 블로그 따라 순조롭게 설치 후 확인 완료

Redis와 분산락

분산락(Distributed Lock)

여러 독립된 프로세스에서 하나의 공유 자원에 접근할 때, 데이터에 결함이 발생하지 않도록 원자성을 보장하기 위해 분산락을 활용

분산락을 구현하기 위해서 redis는 RedLock이라는 알고리즘을 제안하며 3가지의 특성을 보장해야 한다고 말함

  1. 오직 한 순간에 하나의 작업자만이 락을 걸 수 있다.
  2. 락 이후, 어떠한 문제로 인해 락을 풀지 못하고 종료된 경우라도 다른 작업자가 락을 획득할 수 있어야 한다.
  3. 레디스 노드가 작동하는 한 모든 작업자는 락을 걸고, 해체할 수 있어야 한다.

RedLock 알고리즘 수행 과정

RedLock 알고리즘은 비동기식 알고리즘

  1. 현재 시간(ms)을 가져온다.
  2. 모든 인스턴스에서 동일한 키와 랜덤 값을 사용하여 모든 N개의 인스턴스에 순차적으로 잠금을 획득하려고 시도한다.
    • 각 인스턴스에 잠금을 설정할 때 클라이언트는 잠금을 획득하기 위해 전체 잠금 자동 해제 시간에 비해 작은 타임아웃을 사용한다.
    • 예를 들어 자동 해제 시간이 10초인 경우 시간 초과는 ~ 5-50밀리초 범위일 수 있다. 인스턴스를 사용할 수 없는 경우, 최대한 빨리 다음 인스턴스와 연결을 시도해야 한다.
  3. 1단계에서 얻은 타임스탬프를 현재 시간에서 빼서 잠금을 획득하기 위해 경과 시간을 계산한다.
    • 잠금을 획득하는 데 경과 시간이 잠금 유효 시간보다 작으면 잠금이 획득된 것으로 간주한다.
  4. 잠금을 획득한 경우 유효 시간은 3단계에서 계산된 '(초기 유효 시간) - (경과 시간)'으로 간주한다.
  5. 잠금에 실패한 경우(어떤 이유로 잠금을 획득하지 못한 경우(N/2+1개의 인스턴스를 잠글 수 없거나 유효 시간이 음수임), 모든 인스턴스(그렇지 않다고 믿었던 인스턴스도 포함)) 모든 인스턴스에서 잠금을 해제하려고 시도한다.

Redisson

Redis에서 분산락을 구현하기 위해 다양한 구현체를 제공하는데 그 중에 Java는 Redisson 이다.

Build.gradle

dependencies {
  ...
    // redisson
    implementation 'org.redisson:redisson-spring-boot-starter:3.16.3'
    }

BookingService (공연 예약) : 1개의 좌석에 여러 유저가 예약할 수 없다.

1.RedissonClient를 이용하여 seat_lock 이라는 이름의 lock을 생성한다.
2.lock 획득 후 tryLock()을 통해 공유 자원의 접근할 수 있도록 lock을 획득한다.
- lock 획득 실패 시 RuntimeException()을 발생시킨다.
- lock 획득 성공 시 예약 작업을 진행한다.
3.unlock()을 통해 lock을 해제한다.

waitTime 동안 lock 획득 시도, 시간 초과 시 lock 획득 실패하여 false를 return.
lock 획득 성공 시 leaseTime이 지나면 자동으로 lock 해체

private static final int WAIT_TIME = 1;
private static final int LEASE_TIME = 2;
private static final String SEAT_LOCK = "seat_lock";

public Booking saveBookging(final BookingDto reqBooking) {
        RLock lock = redissonClient.getLock(SEAT_LOCK);
        try {
            if (!(lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS))) {
                log.info("lock 획득 실패");
                throw new RuntimeException("Rock fail!");
            }
            log.info("lock 획득");
            ...
            //seat 테이블의 is_booking 칼럼을 true로 update
            updateSeat(seat, performance, reqBooking);

            //seat의 값이 있다면, booking 가능
            bookingHistoryService.saveBookingSucessLog(user, performance, seat);

            //booking 여부 insert
            booking = reqBooking.toEntity(user, performance, seat);
        } catch (InterruptedException e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            lock.unlock();
            log.info("lock 반납");
        }

        return bookingRepository.save(booking);
    }

위 코드는 분산락을 통해 동시성은 보장되지만, 트랜잭션 처리는 되지 않은 코드

RLock

잠금 인터페이스로, 잠금 해제, 획득 시 사용된다.

tryLock(waitTime, leaseTime, unit)

  • waitTime : 잠금을 획득할 수 있는 최대 시간
  • leaseTime : 임대 시간
  • unit : 시간단위

return 타입이 boolean 으로 잠금 획득을 성공하는 경우 true, 실패하는 경우 false를 즉시 반환한다.

잠금이 현재 스레드 또는 다른 프로세스의 스레드에서 의해 유지될 경우, 이 메서드는 fasle를 return 하기 전까지 최대 waitTime까지 잠금 획득을 시도한다.

잠금이 획득되면 unlock이 호출될 때까지 임대 시간 동안 유지된다.

RLock 객체를 이용하여 메서드 사용 시 이전, 도중에 스레드가 중단된 경우 InterruptedException을 발생시킨다. 그래서 꼭 try-catch문으로 lock 처리가 필요하다.

RLock 구현체에 대한 다양한 메소드 존재

void lockInterruptibly(leaseTime, unit) : 잠금 획득. 잠금 사용 불가한 경우 비활성화되고 잠금 획득까지 휴면 상태

void lock(leaseTime, unit) : 잠금 획득. unlock() 호출 전까지 임대 시간 경과전까지 유지. 잠금 사용 불가한 경우 비활성화되고 잠금 획득까지 휴면 상태

void forceUnlock() : 상태에 관계없이 잠금 해제

boolean isLocked() : 잠겨있는지 확인

boolean isHeldByCurrentThread() : 잠금이 현재 스레드에 의해 유지되는지 확인

int getHoldCount() : 현재 스레드에 의한 잠금의 보류 수

Redssion은 pub/sub(발행/구독) 기능을 사용

락이 해제될 때마다 subscribe하는 클라이언트들에게 "너네는 락 획득을 시도해도 된다."라는 알림을 주어 획득 가능한지 여부를 체크하지 않아도 됨.

tryLock()의 동작 방식을 보면 알 수 있음

  1. tryLock 락 획득에 성공하면 true를 반환
    • 이는 경합이 없을 때 아무런 오버헤드 없이 락을 획득할 수 있도록 해줍니다.
  2. pubsub을 이용하여 메세지가 올 때까지 대기하다가 락이 해제되었다는 메세지가 오면 대기를 풀고 다시 락 획득을 시도
    • 락 획득에 실패하면 다시 락 해제 메세지를 대기. 타임아웃시까지 반복
  3. 타임아웃이 지나면 최종적으로 false를 반환, 락 획득 실패를 알림
    • 대기가 풀릴 때 타임아웃 여부를 체크

주의할 점

Redisson은 @Transcational과 동시에 동작하지 않음

@Transcational은 트랜잭션을 AOP 기반으로 돌아가게 만들어진 선언적 트랜잭션 어노테이션.

proxy 객체가 메소드가 종료되었을 때 메소드에 결과에 따라 트랜잭션이 롤백되는지 커밋되는지를 결정. 예외가 던져졌다면 rollback하고, 메소드가 정상적으로 종료됐다면 commit하는 형태

Redisson을 이용하여 분산락을 구현했을 경우, 동시성은 보장되지만 트랜잭션 처리 부분은 빠졌기 때문에 완벽하게 ACID가 보장되는 코드가 아님.

동시에 동작하지 않기 때문에 별도로 transcational manager를 직접 주입하여 비즈니스 코드 자체에서 commit, rollback, start 등을 해줘야 함.

분산락 구현 시에는 unlock()전에 commit 해줘야 함.

log 저장 코드는 1개의 트랜잭션에 넣지 않음

ex. 위에서 작성한 공연 좌석 예약 코드에는 3번의 DB 접속을 포함하고 있음.

1.seat 테이블을 update하는 코드
2.booking log를 save하는 코드 (bookingHistory)
3.booking 테이블에 예약 정보를 save하는 코드

보통 2번과 같이 log를 저장하는 코드는 1개의 트랜잭션에 처리하지 않음.

controller에 성공, 실패에 따른 결과로 호출하도록 하는게 좋은 코드 방식.

Redisson이 Lettuce보다 좋은 이유

1.캐싱 성능

  • Redisson만의 Java에서 캐싱을 수행하는데 도움되는 API를 제공
  • 자주 액세스하는 데이터를 저장하는 기능을 통해 캐싱 성능을 향상
  • Redisson은 또한 read-through, write-through 및 write-behind를 포함한 여러 캐싱 전략을 지원

2.분산 서비스
분산 서비스에 대한 지원을 하여 여러 시스템에 분산 시스템을 구축할 수 있음.

  • RemoteService : Java 원격 메소드 호출.
  • LiveObjectService : 다른 JVM(Java Virtual Machine) 및 다른 컴퓨터에서 공유할 수 있는 향상된 Java 개체인 "라이브 개체"를 생성
  • ExecutorService : 비동기 자바 작업의 진행과 종료를 관리
  • ScheduledExecutorService : 주기적으로 또는 주어진 지연 후에 Java 작업을 예약
  • MapReduce : 매우 많은 양의 데이터를 처리하기 위한 MapReduce 프로그래밍 모델을 구현
    1. 다양한 컬렉션 제공
    2. 커스텀 데이터 직렬화

네트워크를 통해 데이터를 보내기 전에 데이터를 커스텀하여 전송하는 것은 DB 관리를 위한 방법 중 하나.

  • Redisson은 JDK, JSON, Avro, Smile, CBOR, MsgPack, Kryo, FST, LZ4 압축 및 Snappy 압축을 비롯한 다양한 사용자 지정 데이터 직렬화 코덱을 지원.
  • Lettuce는 ByteArrayCodec 및 StringCodec와 같은 코덱에 대한 제한된 직렬화 지원.

참고자료

댓글