동시성 문제에서 Redis의 Lettuce 사용한 이유
- DB를 이용한 동시성 해결방법
DB를 이용하여 분산락을 구현해도 되지만, 락을 잡기위해 간단한 락 정보를 저장하는 테이블을 만들어야 하는게 좋은 선택인지 의문이 들었습니다.
또한, 락 정보는 영구적인 데이터가 아닌 휘발성 데이터에 더 가깝다고 생각하여 DB를 이용하는 것보다 Redis를 이용하는 것이 성능으로 최적화 될 것이라고 생각했습니다.- Redis 이용
Redis가 Single Thread 기반이기 때문에 동시성 제어할 때 좋은 선택지라고 생각했습니다.
락 정보가 간단한 휘발성 데이터에 가깝다고 생각했기 때문에 Redis를 이용하기로 결정
여기서 잠깐, SETNX란?
SET if Not eXist의 줄임말로, 특정 key 값이 존재하지 않을 경우에 set 하라는 명령어 입니다. 특정 키에 대해 SETNX 명령어를 사용하여 value가 없을 때만 값을 세팅하는, 즉 락을 획득하는 효과를 낼 수 있습니다.
Spin Lock 과정
Redis 환경 설정
1. redis 이미지 다운로드 : $ docker pull redis
2. redis 실행 : $ docker run --name myredis -d -p 6379:6379 redis
3. 실행 확인 명령어 : $ docker ps
Spring Redis 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//redis 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
key를 이용한 Lock과 unLock 메소드 정의
@Component
@RequiredArgsConstructor
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
public Boolean lock(final Long key) {
return redisTemplate
.opsForValue()
//setnx 명령어 사용 - key(key) value("lock")
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}
public Boolean unlock(final Long key) {
return redisTemplate.delete(generateKey(key));
}
private String generateKey(final Long key) {
return key.toString();
}
}
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public void decrease(final Long key, final Long quantity) throws InterruptedException {
// Lock 획득 시도
while (!redisLockRepository.lock(key)) {
//SpinLock 방식이 redis 에게 주는 부하를 줄여주기위한 sleep
Thread.sleep(100);
}
//lock 획득 성공시
try{
stockService.decrease(key,quantity);
}finally {
//락 해제
redisLockRepository.unlock(key);
}
}
}
Thread.sleep(100);의 중요성
Sprin Lock 방식이, Lock 을 얻을 떄까지 Lock 얻기를 시도하기 떄문에, 계속해서 Redis 에 접근해서 Redis에 부하를 줄 수 있다는 단점이 존재합니다. 그래서 Thread.sleep(100)를 사용하여 redis에 갈 수 있는 부하를 줄여줍니다.
다음은 테스트 코드입니다. 본인의 테스트 환경에서는 49.581초 소요되었습니다🔽
// StockServiceTest.java
@DisplayName("redis lettuce lock 을 사용한 재고 감소")
// Redis를 사용하면 트랜잭션에 따라 대응되는 현재 트랜잭션 풀 세션 관리를 하지 않아도 되므로 구현이 편리하다.
// Spin Lock 방식이므로 부하를 줄 수 있어서 thread busy waiting을 통하여 요청 간의 시간을 주어야 한다.
@Test
void LETTUCE_LOCK을_사용한_재고_감소() throws InterruptedException {
// given
// when
IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> {
try {
lettuceLockStockFacade.decrease(productId, quantity);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
} finally {
countDownLatch.countDown();
}
}
));
countDownLatch.await();
// then
final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity();
System.out.println("### LETTUCE LOCK 동시성 처리 이후 수량 ###" + afterQuantity);
assertThat(afterQuantity).isZero();
}