동시성 이슈 해결하기 - 2

ttaho·2024년 2월 22일
2

CS

목록 보기
4/4

저번시간에 동시성 이슈가 무엇인지와, 이것을 해결하기 위한 하나의 프로세스, 여러대의 프로세스에서 발생하는 동시성 이슈 해결법에 대해서 알아보았다.

이제는 새로운 방식으로 해결하는 방법인 분산락에 대해서 알아보자.

분산락이란?

분산락은 분산 시스템이나 마이크로서비스 아키텍처와 같이 여러 컴퓨터 또는 프로세스가 네트워크를 통해 서로 통신하며 작동하는 환경에서 사용되는 락이다.

락 상태를 비관적락 이나 낙관적락 처럼 데이터베이스의 내장 기능을 활용하는것이 아니고, 외부 시스템을 사용해서 락의 상태를 관리한다.

Redis 사용하기

분산락을 구현할때, Redis를 사용할 수 있다.
왜 Redis를 사용할까?

DB를 이용하여 분산락을 구현해도 되지만, 락 정보는 영구적인 데이터가 아닌 휘발성 데이터에 더 가깝기 때문이다.
그리고 Redis가 Single Thread 기반이기 때문에 동시성을 제어하기에 좋다.

1. Lettuce

  • Setnx 명령어를 활용하여 분산락을 구현
  • Setnx 는 Spin Lock 방식이므로 retry 로직을 개발자가 직접 작성해야한다.
  • Spin Lock 이란, Lock을 획득하려는 Thread가 Lock을 획득 할 수 있는지 확인하며 반복적으로 시도하는 방법이다.

Setnx란?

SET if Not eXist의 줄임말로, 특정 Key 값이 존재하지 않을 경우에 set 하라는 명령어. 즉, value가 없을 때만 락을 획득하는 효과

코드로 구현해보자.

RedisLockRepository

@Component
public class RedisLockRepository {

    private RedisTemplate<String, String> redisTemplate;

    public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Boolean lock(Long key){
        return redisTemplate
                .opsForValue()
                .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    }

    public Boolean unlock(Long key){
        return redisTemplate.delete(generateKey(key));
    }
    private String generateKey(Long key) {
        return key.toString();
    }
}
  1. SpinLock 방식으로 락을 얻기를 시도하고,
  2. 락을 얻은 후, 재고 감소 비즈니스 로직을 처리
  3. 그 후, 락을 해제해주는 방식이 Lettuce 방식

퍼사드 패턴을 사용하여 RedisLockRepository와 StockService를 주입.

@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)은
Spin Lock 방식이 Lock을 얻을때까지 계속 시도하는데, 이때 Redis의 부하를 줄여주기 위해 사용.

2. Redisson

Redisson은 pub/sub 기능을 사용하여 스핀 락이 레디스에 주는 트래픽을 줄였다.
락이 해제될 대마다 subscribe하는 클라이언트들에게 "락 획득을 시도하세요" 라는 알림을 주어서 각 쓰레드가 레디스에 요청을 보내 락의 획득가능여부를 체크하지 않아도 되도록 개선한 방식이다.

또한, Redisson은 RLock 이라는 락을 위한 인터페이스를 제공하여 손쉽게 락을 사용할 수 있다.

RedissonLockStockFacade

@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {

    private final RedissonClient redissonClient;
    private final StockService stockService;

    @Transactional
    public void decrease(Long id, Long quantity){
        RLock lock = redissonClient.getLock(id.toString());

        try{
            boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);

            if( !available){
                System.out.println("Lock 획득 실패");
                return;
            }

            stockService.decrease(id, quantity);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}
  1. redissonClient의 getLock을 통해 해당하는 id에 맞는 RLock 객체를 획득
  2. tryLock을 통해 실제 락 획득 10초 동안 시도하고, 락을 획득하면 1초 유지.
  3. 감소로직 수행하고 락 해제

동일하게 테스트를 진행하였다.

profile
백엔드 꿈나무

0개의 댓글