(Lock보다는 Rock!)
Redis를 활용하기 위해서는 몇 가지 준비를 해야 한다.
docker pull redis
docker run --name myredis -d -p 6379:6379 redis
나는 강의를 통해 도커 환경에서 레디스를 구동시켰다.
동일한 환경이라는 가정 하에 위와 같은 명령어를 터미널에 입력해주자.
docker ps
그리고 "ps" 명령어를 통해
레디스 프로세스가 작동중인지 확인할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.gradle
에 위와 같이 의존성을 추가해주자.
(막간을 활용한 마블발 추천 !!!)
Lettuce는 Redis를 자바 어플리케이션 상에서
활용할 수 있도록 돕는 Redis 클라이언트다.
docker 컨테이너 식별자를 넣어서
Redis를 실행해본다.
그리고 "setnx" 명령어를 통해 (SET if Not eXists)
key가 1이고 value가 'lock'인 데이터를 생성해본다.
처음에는 아무런 데이터가 없었으므로 생성에
성공했다는 의미의 1을 받았지만,
다시 시도하면 이미 데이터가 있기 때문에 생성에
실패했다는 의미의 0을 받는다.
@RequiredArgsConstructor
@Repository
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
.setIfAbsent(key.toString(), "lock", Duration.ofMillis(3_000));
}
public void unlock(Long key) {
redisTemplate
.delete(key.toString());
}
}
RedisTemplate
을 활용해서 Redis에
lock과 unlock을 호출해준다.
setIfAbsent
는 "setnx" 명령어와 동일한 일을 수행한다.
그리고 파라미터로 key, value, timeout
을 받는다.
delete
는 "del" 명령어와 동일한 일을 수행한다.
그리고 파라미터로 key
를 받는다.
@RequiredArgsConstructor
@Component
public class LettuceLockStockFacade {
private final RedisLockRepository repository;
private final StockService stockService;
public void decrease(Long id, Long quantity) throws InterruptedException {
while (!repository.lock(id)) {
Thread.sleep(100);
}
stockService.decrease(id, quantity);
repository.unlock(id);
}
}
Lettuce를 활용한 방식 역시 파사드 클래스를 필요로 한다.
(
생각해보면 당연한 것이,
"setnx 1 lock"을 A와 B가 순서대로 입력했다고 해보자.
그러면 B는 0을 반환받고 다시 A가 "del 1"을 입력할 때까지
지속적으로 "setnx 1 lock"을 입력해야 한다.
)
그래서 while 문을 활용해 lock이 풀릴 때까지 중간중간 sleep하면서 기다린다.
lock을 획득하면 decrease 후에 unlock한다.
장점
-> lock을 얻기 위해 기다리는 로직을 작성하지 않아도 된다.
-> pub-sub 방식을 활용하기 때문에 스핀락이 없고, 그래서 redis에 부담이 덜 간다.
단점
-> redisson 라이브러리 차원의 lock이 제공되기 때문에 이에 대한 주의가 필요하다.
(한국 최후의 록스타 서태지)
Lettuce에서와 동일하게 redis 실행 환경으로 접속해야 한다.
그리고 subscribe ch1
을 입력하면,
상단 이미지처럼 메시지를 기다린다.
다른 터미널 창에서 동일한 실행 환경으로,
publish ch1 helloworld
를 입력하니
첫번째 사진에서 동일한 내용의 메시지를
받은 것을 확인할 수 있었다.
implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
먼저 build.gradle
에 Redisson을 사용하기 위한
의존성을 추가해줘야 한다.
@RequiredArgsConstructor
@Component
public class RedissonLockStockFacade {
private final RedissonClient redissonClient;
private final StockService stockService;
public void decrease(Long id, Long quantity) {
RLock lock = redissonClient.getLock(id.toString());
try {
boolean available = lock.tryLock(30, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("락 획득 실패");
return;
}
stockService.decrease(id, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
그리고 RedissonClient
객체를 주입받아
getLock
으로 얻어온 락에 대해
tryLock
과 unlock
을 호출하면 된다.
(
tryLock
에서 각 파라미터는 다음과 같다.
[0] : lock을 얻기 위한 최대 시간
[1] : lock을 얻을 수 있는 스레드 수
[2] : [0]번째 매개 변수에 대한 시간 단위 지정
)
장점
-> lock을 얻기 위해 기다리는 로직을 작성하지 않아도 된다.
-> pub-sub 방식을 활용하기 때문에 스핀락이 없고, 그래서 redis에 부담이 덜 간다.
단점
-> redisson 라이브러리 차원의 lock이 제공되기 때문에 이에 대한 주의가 필요하다.
(그래서?)
이번 "동시성 이슈 해결" 시리즈를 통해
약 6가지 방법으로 멀티 스레드를 적절히 처리하는 방법을 배웠다.
위 예제는 너무 간단한 케이스인데다,
실제로 사용자가 짧은 시간 안에 다발적으로 요청을 보내올 때에는
어떻게 로직이 작동하는지 알 수 없으므로
어떤 방법이 최선이라고 하기는 어려운 것 같다.
그래서 내가 생각하기에는, 장단점을 알고 있는 상태에서
실제로 내가 동시성을 처리해야 하는 문제에 여러 방법을 모두 적용해보는 수밖에 없는 것 같다.
🦾 참고 🦾
위 글은 인프런에서 상용 님의
'재고시스템으로 알아보는 동시성이슈 해결방법' 수업을 참고하여 작성되었습니다.