-
Notifications
You must be signed in to change notification settings - Fork 1
redis 설치 및 redisson을 이용한 분산락 구현
redis 설치
위 블로그 따라 순조롭게 설치 후 확인 완료
분산락(Distributed Lock)
여러 독립된 프로세스에서 하나의 공유 자원에 접근할 때, 데이터에 결함이 발생하지 않도록 원자성을 보장하기 위해 분산락을 활용
분산락을 구현하기 위해서 redis는 RedLock이라는 알고리즘을 제안하며 3가지의 특성을 보장해야 한다고 말함
- 오직 한 순간에 하나의 작업자만이 락을 걸 수 있다.
- 락 이후, 어떠한 문제로 인해 락을 풀지 못하고 종료된 경우라도 다른 작업자가 락을 획득할 수 있어야 한다.
- 레디스 노드가 작동하는 한 모든 작업자는 락을 걸고, 해체할 수 있어야 한다.
RedLock 알고리즘은 비동기식 알고리즘
- 현재 시간(ms)을 가져온다.
- 모든 인스턴스에서 동일한 키와 랜덤 값을 사용하여 모든 N개의 인스턴스에 순차적으로 잠금을 획득하려고 시도한다.
- 각 인스턴스에 잠금을 설정할 때 클라이언트는 잠금을 획득하기 위해 전체 잠금 자동 해제 시간에 비해 작은 타임아웃을 사용한다. + 예를 들어 자동 해제 시간이 10초인 경우 시간 초과는 ~ 5-50밀리초 범위일 수 있다. 인스턴스를 사용할 수 없는 경우, 최대한 빨리 다음 인스턴스와 연결을 시도해야 한다.
- 1단계에서 얻은 타임스탬프를 현재 시간에서 빼서 잠금을 획득하기 위해 경과 시간을 계산한다.
- 잠금을 획득하는 데 경과 시간이 잠금 유효 시간보다 작으면 잠금이 획득된 것으로 간주한다.
- 잠금을 획득한 경우 유효 시간은 3단계에서 계산된 '(초기 유효 시간) - (경과 시간)'으로 간주한다.
- 잠금에 실패한 경우(어떤 이유로 잠금을 획득하지 못한 경우(N/2+1개의 인스턴스를 잠글 수 없거나 유효 시간이 음수임), 모든 인스턴스(그렇지 않다고 믿었던 인스턴스도 포함)) 모든 인스턴스에서 잠금을 해제하려고 시도한다.
Redis에서 분산락을 구현하기 위해 다양한 구현체를 제공하는데 그 중에 Java는 Redisson 이다.
Build.gradle
dependencies {
...
// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.16.3'
}
BookingService (공연 예약)
1개의 좌석에 여러 유저가 예약할 수 없다.
- RedissonClient를 이용하여 seat_lock 이라는 이름의 lock을 생성한다.
- lock 획득 후 tryLock()을 통해 공유 자원의 접근할 수 있도록 lock을 획득한다.
- lock 획득 실패 시 RuntimeException()을 발생시킨다.
- lock 획득 성공 시 예약 작업을 진행한다.
- 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);
}
위 코드는 분산락을 통해 동시성은 보장되지만, 트랜잭션 처리는 되지 않은 코드
잠금 인터페이스로, 잠금 해제, 획득 시 사용된다.
-
waitTime
: 잠금을 획득할 수 있는 최대 시간 -
leaseTime
: 임대 시간 -
unit
: 시간단위
return 타입이 boolean 으로 잠금 획득을 성공하는 경우 true, 실패하는 경우 false를 즉시 반환한다.
잠금이 현재 스레드 또는 다른 프로세스의 스레드에서 의해 유지될 경우, 이 메서드는 fasle를 return 하기 전까지 최대 waitTime까지 잠금 획득을 시도한다.
잠금이 획득되면 unlock이 호출될 때까지 임대 시간 동안 유지된다.
RLock 객체를 이용하여 메서드 사용 시 이전, 도중에 스레드가 중단된 경우 InterruptedException
을 발생시킨다. 그래서 꼭 try-catch문으로 lock 처리가 필요하다.
void
lockInterruptibly(leaseTime, unit)
: 잠금 획득. 잠금 사용 불가한 경우 비활성화되고 잠금 획득까지 휴면 상태void
lock(leaseTime, unit)
: 잠금 획득. unlock() 호출 전까지 임대 시간 경과전까지 유지. 잠금 사용 불가한 경우 비활성화되고 잠금 획득까지 휴면 상태void
forceUnlock()
: 상태에 관계없이 잠금 해제boolean
isLocked()
: 잠겨있는지 확인boolean
isHeldByCurrentThread()
: 잠금이 현재 스레드에 의해 유지되는지 확인int
getHoldCount()
: 현재 스레드에 의한 잠금의 보류 수
락이 해제될 때마다 subscribe하는 클라이언트들에게 "너네는 락 획득을 시도해도 된다."라는 알림을 주어 획득 가능한지 여부를 체크하지 않아도 됨.
tryLock()의 동작 방식을 보면 알 수 있음
-
tryLock
락 획득에 성공하면true
를 반환- 이는 경합이 없을 때 아무런 오버헤드 없이 락을 획득할 수 있도록 해줍니다.
- pubsub을 이용하여 메세지가 올 때까지 대기하다가 락이 해제되었다는 메세지가 오면 대기를 풀고 다시 락 획득을 시도
- 락 획득에 실패하면 다시 락 해제 메세지를 대기. 타임아웃시까지 반복
- 타임아웃이 지나면 최종적으로
false
를 반환, 락 획득 실패를 알림- 대기가 풀릴 때 타임아웃 여부를 체크
@Transcational은 트랜잭션을 AOP 기반으로 돌아가게 만들어진 선언적 트랜잭션 어노테이션.
proxy 객체가 메소드가 종료되었을 때 메소드에 결과에 따라 트랜잭션이 롤백되는지 커밋되는지를 결정. 예외가 던져졌다면 rollback하고, 메소드가 정상적으로 종료됐다면 commit하는 형태
Redisson을 이용하여 분산락을 구현했을 경우, 동시성은 보장되지만 트랜잭션 처리 부분은 빠졌기 때문에 완벽하게 ACID가 보장되는 코드가 아님.
동시에 동작하지 않기 때문에 별도로 transcational manager를 직접 주입하여 비즈니스 코드 자체에서 commit, rollback, start 등을 해줘야 함.
분산락 구현 시에는 unlock()전에 commit 해줘야 함.
ex. 위에서 작성한 공연 좌석 예약 코드에는 3번의 DB 접속을 포함하고 있음.
- seat 테이블을 update하는 코드
- booking log를 save하는 코드 (bookingHistory)
- booking 테이블에 예약 정보를 save하는 코드
보통 2번과 같이 log를 저장하는 코드는 1개의 트랜잭션에 처리하지 않음.
controller에 성공, 실패에 따른 결과로 호출하도록 하는게 좋은 코드 방식.
- 캐싱 성능
- Redisson만의 Java에서 캐싱을 수행하는데 도움되는 API를 제공
- 자주 액세스하는 데이터를 저장하는 기능을 통해 캐싱 성능을 향상
- Redisson은 또한 read-through, write-through 및 write-behind를 포함한 여러 캐싱 전략을 지원
- 분산 서비스
분산 서비스에 대한 지원을 하여 여러 시스템에 분산 시스템을 구축할 수 있음.
- RemoteService : Java 원격 메소드 호출.
- LiveObjectService : 다른 JVM(Java Virtual Machine) 및 다른 컴퓨터에서 공유할 수 있는 향상된 Java 개체인 "라이브 개체"를 생성
- ExecutorService : 비동기 자바 작업의 진행과 종료를 관리
- ScheduledExecutorService : 주기적으로 또는 주어진 지연 후에 Java 작업을 예약
- MapReduce : 매우 많은 양의 데이터를 처리하기 위한 MapReduce 프로그래밍 모델을 구현
- 다양한 컬렉션 제공
- 커스텀 데이터 직렬화
네트워크를 통해 데이터를 보내기 전에 데이터를 커스텀하여 전송하는 것은 DB 관리를 위한 방법 중 하나.
- Redisson은 JDK, JSON, Avro, Smile, CBOR, MsgPack, Kryo, FST, LZ4 압축 및 Snappy 압축을 비롯한 다양한 사용자 지정 데이터 직렬화 코덱을 지원.
- Lettuce는 ByteArrayCodec 및 StringCodec와 같은 코덱에 대한 제한된 직렬화 지원.
- RDBMS의 트랜잭션과 병행제어
- Spring/Spring Boot/Spring MVC
- Spring의 Transaction
- 동시성 문제
- Redis 란?
- Spring Boot 로그설정(logback)
- 빠르게 실패 vs 안전하게 실패
- 테스트 코드 작성 시 유의사항
- Unit Test에서 AssertThat을 사용
- Java Optional 바르게 쓰기
- java.util.Optional T 클래스
- @Validation 어노테이션
- @RequestParam Date 타입 받기
- Spring Data JPA 쿼리메소드
- [JUnit5] 기본 테스트 어노테이션
- redis 설치 및 redisson을 이용한 분산락 구현