(3) 동시성 이슈 - Redis(with MySQL VS Redis)

alsdl0629·2023년 12월 14일
0

동시성 이슈

목록 보기
3/4
post-thumbnail

이번 글에서는 최상용님의 "재고시스템으로 알아보는 동시성이슈 해결방법"을 정리해 보려고 합니다.

실습한 레포지토리 👈

Redis에서 지원하는 방법으로 문제 해결

1. Lettuce

Lettuce는 분산락을 구현할 때 사용하는 대표적인 라이브러리입니다.
SETNX를 활용해 분산락을 구현합니다.

SETNX = set if not exists 의 줄임말으로,
키가 존재하지 않을 경우 값을 지정하는 방식으로 동작합니다.

또한, 스레드가 락을 사용할 수 있는지 반복적으로 확인하면서 락 획득을 시도하는
spin lock 방식을 사용합니다.

이때 retry 로직은 개발자가 작성해줘야 합니다.


터미널에서 실습

Redis-CLI에 접속해서 데이터가 없는 상태에서
setnx 1 lock 명령어를 실행하면

키가 1인 데이터가 없기 때문에 성공합니다.

다시 setnx 1 lock 명령어를 실행하면?

키가 1인 데이터가 존재하기 때문에 실패하게 됩니다.


@Component
public class RedisLockRepository {

    private final 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(3000));
    }

    public Boolean unlock(Long key) {
        return redisTemplate.delete(generateKey(key));
    }

    private String generateKey(Long key) {
        return key.toString();
    }
}
@Component
public class LettuceLockStockFacade {

    private final RedisLockRepository redisLockRepository;

    private final StockService stockService;

    public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
        this.redisLockRepository = redisLockRepository;
        this.stockService = stockService;
    }

    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (!redisLockRepository.lock(id)) {
            /**
             * Lock 획득에 실패하면 Thread.sleep()을 사용해서 100ms 텀을 두고, 재시도
             * 레디스의 부하를 줄임
             */
            Thread.sleep(100);
        }

        try {
            stockService.decrease(id, quantity);
        } finally {
            redisLockRepository.unlock(id);
        }
    }
}

LettuceNamed Lock과 비슷하지만,
세션 관리를 신경쓰지 않아도 된다는 점이 다릅니다.


Lettuce 장점

  • 구현이 간단합니다.
  • spring data redis는 기본 구현체가 Lettuce이기 때문에 별도의 라이브러리를 사용하지 않아도 됩니다.

Lettuce 단점

  • spin lock 방식때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있습니다. 그렇기 때문에 스레드간의 락 획득 텀을 두어야 합니다.

Redisson

RedissonPUB/SUB 기반의 Lock 구현을 제공합니다.

PUB-SUB 기반은 채널을 하나 만들고 락을 점유 중인 스레드가 락을 획득하려고 대기 중인 스레드에게 해체를 알려주면 안내를 받은 스레드가 락 획득 시도를 하는 방식입니다.

그렇기 때문에 retry 로직이 필요하지 않습니다.

터미널에서 실습

메시지를 주고 받기 위해 터미널을 두 개 띄워줍니다.

ch1이라는 채널을 구독합니다.

다른 터미널에서 hello라는 메시지를 ch1 채널에 보내줍니다.

ch1을 구독하고 있는 터미널에서 메시지를 받을 수 있습니다.

// Redisson을 사용하기 위해 추가
implementation 'org.redisson:redisson-spring-boot-starter:3.25.0'

Redisson 장점

  • Lettuce는 계속 락 획득을 시도하는 반면에 Redisson락 해제가 되었을 때만 시도(PUB-SUB 기반의 구현)하기 때문에 레디스의 부하를 줄여줍니다.

Redisson 단점

  • 구현이 복잡합니다.
  • 별도의 라이브러리인 RedissonClient를 사용해야 합니다.

Lettuce VS Redisson

Lettuce : 재시도가 필요하지 않은 경우에 사용
Redisson : 재시도가 필요한 경우에 사용


MySQL VS Redis

MySQL

  • 이미 MySQL을 사용하고 있다면 별도의 비용없이 사용 가능합니다.
  • 어느 정도의 트래픽까지는 문제없이 활용 가능합니다.
  • 하지만 Redis 보다 성능이 좋지 않습니다.

Redis

  • MySQL 보다 성능이 좋기 때문에 더 많은 요청을 처리할 수 있습니다.
  • 기존에 Redis를 사용하지 않는다면 구축 비용과 관리 비용이 발생합니다.

실무에서는

  • 비용적 여유가 없거나 MySQL로 처리가 가능할 정도의 트래픽이라면 MySQL을 사용
  • 비용적 여유가 있거나 MySQL로 처리가 불가능할 정도의 트래픽이라면 Redis를 도입
profile
인풋보다 아웃풋

0개의 댓글