Intro
- Redis는 싱글 쓰레드로 동작하기 때문에 단일 노드로 구축해 사용해도 동시성 문제가 발생하지 않는다.
- SPOF(Single Point Of Failure) : 단일 장애 지점
- Replication : Master - Slave - Slave의 구조
- Failover(장애조치) : 현재 마스터를 복제복으로 바꾸고 복제본을 마스터로 변경한다. 마스터에서 실행하는 것
- Redis의 복제는 비동기식이기 때문에 상황에 따라 경합(race condition)이 발생할 수 있다. (키에 대한 쓰기가 복제본으로 전송되기 전 마스터 다운)
- Race Condition(경쟁 상황) : 2개 이상의 프로세스가 동시에 같은 자원에 접근하여 read, write 하는 상황
- 임계구역 : 둘 이상의 쓰레드가 접근해서는 안되는 공유 자원을 접근하는 코드의 일부. 임계 구역은 지정된 시간이 지난 후 종료되며 때문에 어떤 쓰레드가 임계 구역에 들어가고자 한다면 지정된 시간만큼 대기해야한다.
- Distributed Lock(분산락) : 경쟁 상황이 발생했을 때 혹은 하나의 공유자원에 접근할 때 데이터에 결함이 발생하지 않도록 원자성을 보장하는 기법.
- SET 명령어
- NX(Not eXists) 옵션 : 키가 존재하지 않을 때만 SET
- PX 옵션 : ms로 유효 시간 조정
- SETNX(Set if Not eXists) 명령어 : 키가 없는 경우에만 SET이 성공
- Lock : 공유 자원에 대한 동시 접근을 제어하는 매커니즘. 하나의 쓰레드만 해당 자원에 접근, 변경 할 수 있다.
- 비관적 락은 읽기조차 불가능
RedLock 알고리즘
과반 수 이상의 노드에서 Lock을 획득했다면 Lock을 획득한 것으로 간주
분산환경에서 Redis가 권장하는 방법이다.
- 현재 시간을 ms 단위로 구한다.
- 순차적으로 N대의 Redis 서버에 잠금을 요청한다. 이 때 timeout은 Lock의 유효 시간 보다 훨씬 작은 시간을 쓴다. 만약 Lock의 유효 시간이 10초라면 각 Redis 서버에 잠금을 획득하기 위한 timeout은 5~50ms 이다. 이렇게 짧은 timeout을 사용해 장애가 발생한 Redis 서버와 통신에 많은 시간을 사용하지 않도록 방지할 수 있다.
- Redis 서버가 5대라고 가정할 때 과반수(3대) 이상의 서버로부터 Lock을 획득했고 Lock을 획득하기 위해 사용한 시간이 Lock의 유효 시간보다 작았다면 Lock을 획득했다고 간주한다. 즉 Lock의 유효시간이 10초인데 Lock을 얻기 위해 11초가 걸렸다면 실패한 것이다.
- Lock을 획득 한 후 유효시간은 처음 Lock의 유효시간 - Lock을 얻기 위해 걸린 시간이다. Lock의 유효 시간이 10초인데 획득에 3초가 걸렸다면 얻은 후부터 7초 뒤에 만료된다.
- Lock을 얻지 못했다면 모든 Redis 서버에게 Lock 해제 요청을 보낸다. 예를 들어 5대의 Redis 서버 중 한 대의 서버에게만 Lock 획득을 성공했다. 과반 수 이상의 Lock을 획득하지 못했으므로 Lock 획득에 실패했고 모든 Redis 서버에 Lock 해제 요청을 보낸다.
요약 →
- Client는 Lock을 획득하기 위해 모든 Redis 서버에게 Lock을 요청. 과반 수 이상의 Redis 서버에게 Lock을 획득하면 Lock을 획득
- 실패했다면 모든 Redis 서버에게 Lock 해제를 요청하고 일정 시간 후에 Lock을 획득하기 위한 재 시도를 한다.
완벽하진 않음
GC, 네트워크 지연, 타이밍 이슈로 깨질 수 있음
Lettuce
Redis의 java 구현체
SETNX 명령어와 같은 원자적 연산을 사용해 스핀락 기법으로 분산락을 구현해야한다.
- Spin Lock :멀티 스레딩 환경에서 공유 자원에 대한 동시 접근을 방지 하기 위한 락 중 하나.
다른 락과는 다르게, 락을 획득할 대까지 계속해서 락 획득을 시도하고 조건을 확인하면서 대기하는 기법.
1. A스레드가 락을 획득하려고 시도 2. 락이 이미 다른 스레드가 획득했다면 A스레드가 반복적으로 요청하면서 락 획득 시도 3. 락이 해제되면 다음으로 먼저 요청한 스레드중 하나가 랜덤으로 락을 획득
지속적으로 Retry 하기 때문에 성능 이슈가 발생한다.
Redisson
pub/sub 방식을 이용한다.
Lock이 해제될 때마다 subscribe 중인 클라이언트에게 "이제 락 획득을 시도해도 된다" 라는 알림을 보낸다.
따라서 클라이언트에서 락 획득을 실패했을 때 redis에 지속적으로 락 획득 요청을 보내는 과정이 사라지고 이에 따라 부하가 발생하지 않게 된다.
또 Ridesson은 RLock이라는 락을 위한 인터페이스를 제공함.
RLock rLock = redissonClient.getLock(lockName);
try {
boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);
if(!available){
return false; //락 획득 실패
}
//락 획득 후 로직 수행
}catch (InterruptedException e){
//락을 얻으려고 시도하다가 인터럽트를 받았을 때 발생하는 예외
}finally{
try{
rLock.unlock();
log.info("unlock complete: {}", rLock.getName());
}catch (IllegalMonitorStateException e){
//이미 종료된 락일 때 발생하는 예외
}
}
<참고>
[Redis] 레디스가 제공하는 분산락(RedLock)의 특징과 한계
레디스를 활용한 분산 락(Distrubuted Lock) feat lettuce, redisson
풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson (마켓컬리)
Distributed Lock 구현 과정 (채널톡에서 Redisson pub/sub 사용)
'Back-end' 카테고리의 다른 글
[Spring/Thymeleaf] option 태그에 enum 동적으로 넣기 (0) | 2024.08.10 |
---|---|
[Redis] Cache 전략 (0) | 2024.07.29 |
[C#] using 구문 (1) | 2022.09.08 |
[Node.js] NVM으로 여러 버전의 node.js 사용하기 (0) | 2022.07.20 |
[C#] ??(null 병합 연산자 ), ??=(null 병합 할당 연산자), ?.(null 조건 연산자) (0) | 2022.06.20 |