Redis Template을 활용하여 동시성 문제를 해결한다.
redistemplate
에 있는 락을 활용해서 문제를 해결한다.
락을 건다라는 표현이라기 보다, orderId
로 활용중인 자원을 redis
에 저장해서 관리한다.
처음에는 Redis
에서 별도로 Lock
에 관련된 기능을 제공하는 줄 알았는데, 그건 아니었다. Redis
는 key-value
로 이루어진 딕셔너리 구조이기 때문에 이 딕셔너리에 일종의 접근/변경하면 안되는, 즉 lock이 걸린 것처럼 동작해야하는 공유자원(어떤 트랜잭션이 동작하고 있는 공유자원)의 값들(그 공유자원을 특정할 수 있는 값)을 lock이름
- 해당 공유자원의 키
로 저장해두고 동시성 문제를 유발할 수 있는 method에서 Redis
내에 현재 그 키값이 존재한다면(즉, 키 값의 expire time이 지나지 않아 남아있다면) 해당 행위를 대기열에 넣어 미루겠다는 말을 의미한다.
곧 그렇게 되면, expire time이 지난다면 트랜잭션은 끝나있을 것이고 expire시간 안에 잘 동작했다면 동시성이슈로 발생할 수 있는 문제점은 뒤에 시작된 트랜잭션이 먼저 발생한 트랜잭션을 침해하지 않게 되기 때문에 발생하지 않게 된다.
결국에는 Lock
과 같은 기능을 구현할 수 있게 되는 것이다. 이런 방식은 비관적 락(pessimatic locking)에 해당하며 데이터 또는 자원에 대한 접근을 미리 차단하는 방식이 된다. 전통적인 방식은 락을 “획득”하는 것인데 여기서는 “공유 자원 아이디의 존재 여부를 확인”하는 접근 제어를 수행하고 있는 것이다. 그렇지만, 공유 자원에 대한 동시 접근을 방지한다는 점에서 비관적 락의 기본 원리를 따르고 있기에 비관적 락의 개념에 가깝다고 생각하면 된다.
반대로 낙관적 락은 데이터를 수정하기 전에 데이터가 변경되지 않았음을 가정하고 실제 데이터를 업데이트 했을 때만 충돌이 발생하엿는지 확인하는 방식이다. 주로 타임스탬프 또는 버전을 사용하며, 업데이트 할 때 불러온 버전과 현재 데이터의 버전이 일치하는지 확인하고 일치하지 않는다면 업데이트를 중단시키는 것이다.
1. 오더 취소하는 과정에서 생길 수 있는 동시성 문제
1) 주문 실패 오더: 주문 취소서를 작성하고 있는 과정에서 주문 확정이 될 수 있는 점
- 처음 생각: 취소 폼을 가져온 순간 2분간 스케줄링에서 매칭상태를 변경할 수 없도록 lock을 걸어둔다.
- 실제로 취소를 신청할 때의 상태가 최종적으로 확인되면 된다고 본다. → 결론: 필요 x
- 실제로 취소를 신청할 때의 상태가 최종적으로 확인된다는 것: 오더 취소폼 작성 중에 주문 확정에 성공했을 경우, 성공했다는 것을 알려주고 그래도 취소할 것이냐(수수료부과)를 물어보는게 맞다고 판단
2) 주문 성공 오더: 주문 확정이 성공했는데 주문자가 추후 취소를 요청하는 과정에서 가게주인이 먼저 취소 요청을 처리해버린 경우. 혹은 반대의 경우
- 화주/차주가 취소를 전송할 때 lock을 걸어둔다.
- 화주/차주 트랜잭션이 종료되기 전에 상대의 트랜잭션이 시작될 수 있는 문제! (동시성 문제)
- 락이 필요하다!
2. Redis Template에서 timeoutSeconds의 의미
Redis Template에서 timeoutSeconds
또는 일반적으로 사용되는 "expire time"은 락(lock)에 설정하는 만료 시간을 의미한다. 이 만료 시간은 락이 자동으로 해제되기까지의 시간을 지정합니다. 락의 만료 시간 설정은 분산 시스템에서 매우 중요한 역할을 하며, 다음과 같은 이유로 사용된다:
- 데드락 방지: 시스템 내에서 예기치 않은 이유로 락을 해제하는 코드가 실행되지 않았을 때, 데드락(deadlock) 상태에 빠질 수 있습니다. 데드락이란 두 개 이상의 프로세스가 서로가 보유한 자원의 해제를 무한히 기다리는 상태를 말합니다. 만료 시간을 설정함으로써, 특정 시간이 지나면 자동으로 락이 해제되어 이러한 데드락 상태를 방지할 수 있습니다.
- 자원의 효율적 사용: 락이 너무 오랫동안 유지되면, 다른 프로세스나 스레드가 필요한 자원에 접근하는 데 지연이 발생할 수 있습니다. 만료 시간을 설정함으로써, 락이 필요한 시간 동안만 유지되도록 하여 자원을 보다 효율적으로 사용할 수 있습니다.
시나리오 예시
예를 들어, 배달 어플에서 가게/손님이 오더를 취소하려고 시도한다고 가정해 보자. 이 상황에서 락의 만료 시간은 다음과 같은 역할을 한다:
- 첫 번째 시도: 손님이 오더 취소를 위해 락을 요청하고, 성공적으로 락을 획득한다. 이 때,
timeoutSeconds
로 설정된 만료 시간이 락에 적용된다. 만약 이 시간이 30초라면, 손님은 30초 동안 오더 취소 작업을 완료할 수 있다. - 두 번째 시도: 손님과 가게주인이 거의 동시에 같은 오더에 대해 취소를 시도하지만, 손님이 이미 락을 획득했기 때문에 화주의 시도는 대기 상태에 들어간다.
- 만료 시간의 역할: 손님이가 어떤 이유로 오더 취소 작업을 30초 안에 완료하지 못하고 락을 명시적으로 해제하지 않았다면, 설정된 만료 시간이 지나면 자동으로 락이 해제된다. 이로 인해 가게 주인은 락을 획득할 수 있는 기회를 갖게 되며, 시스템은 데드락 상태에 빠지지 않고 자원을 계속해서 효율적으로 사용할 수 있게 된다.
이 예시에서 락의 만료 시간은 중요한 두 가지 기능을 수행한다.
첫째, 예기치 않은 상황에서 시스템이 데드락에 빠지는 것을 방지하고, 둘째, 모든 사용자가 공정하게 자원에 접근할 수 있도록 한다. 이와 같이 만료 시간 설정은 분산 시스템에서 락을 사용할 때 발생할 수 있는 다양한 문제를 예방하는 데 중요한 역할을 한다.
'Backend > Trouble Shooting' 카테고리의 다른 글
Java + Kotlin 프로젝트 QueryDSL QFile을 찾지 못하는 이슈(소스 세트 구성 오류) (1) | 2024.03.29 |
---|---|
java.io.StreamCorruptedException: invalid stream header: 30313031 (공간 데이터 처리) (0) | 2024.03.18 |