본문 바로가기
개발/Spring

[Spring] 동시성 이슈 해결 방법 (4)

by devhooney 2022. 9. 26.
728x90

지난번 포스팅과 코드를 이어간다.

 

https://devhooney.tistory.com/110

 

[Spring] 동시성 이슈 해결 방법 (3)

지난번 포스팅과 코드를 이어간다. https://devhooney.tistory.com/109 [Spring] 동시성 이슈 해결 방법 (2) 지난번 포스팅과 코드를 이어간다. https://devhooney.tistory.com/108 [Spring] 동시성 이슈 해결 방..

devhooney.tistory.com

 

메모리 DB인 Redis를 활용하여 데이터 정합성을 맞추는 방법을 알아보자

 

Lettuce

Redisson

 

2가지가 있다.

 

Lettuce

- Lettuce는 setnx 명령어를 활용하여 분산락을 구현한다.

키와 밸류를 set할 때 기존 값이 없을 때만 set하는 명령어

spin lock 방식 이므로 재시도 로직을 직접 짜야 한다.

spin lock이란 쓰레드가 lock을 사용할 수 있는지 반복적으로 확인하면서 시도하는 방식을 말한다.

- 쓰레드1이 키가 1인 데이터를 레디스에 set하면 처음엔 1이 없으므로 성공한다.

- 쓰레드2가 키가 1인 데이터를 set하려 하면 1이 이미 있으므로 실패한다.

- 성공을 위해 100ms마다 재시도하는 로직을 작성해서 성공할 때 까지 시도한다.

 

Redisson

- Redisson은 pub-sub 기반의 lock 구현을 제공한다.

pub-sub 기반은 채널을 하나 만들고 lock을 점유중인 쓰레드가 다음 lock을 점유하려는 쓰레드에게 점유가 끝났음을 알려주면서 lock을 주고 받는 방식이다.

Lettuce 코드

아래 라이브러리를 build.gradle에 작성하여 설치한다.(라이브러리 설치 전 레디스를 먼저 설치한다.)

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

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));
    }

    public String generateKey(Long key) {
        return key.toString();
    }
}

 

로직 실행 전 후로 Lock 획득과 해제를 수행해야 하므로 Facade 클래스를 추가한다.

@Component
public class LettuceLockStockFacade {
    private RedisLockRepository redisLockRepository;

    private StockService stockService;

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

    public void decrease(Long key, Long quantity) throws InterruptedException {
        while (!redisLockRepository.lock(key)) {
            Thread.sleep(100);
        }

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

 

Test코드

@SpringBootTest
class LettuceLockStockFacadeTest {
    @Autowired
    private LettuceLockStockFacade lettuceLockStockFacade;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before() {
        Stock stock = new Stock(1L, 100L);

        stockRepository.saveAndFlush(stock);
    }

    @AfterEach
    public void after() {
        stockRepository.deleteAll();
    }


    @Test
    public void 동시에_100개의_요청() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                try {
                    lettuceLockStockFacade.decrease(1L, 1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    {
                        latch.countDown();
                    }
                }
            });
        }

        latch.await();

        Stock stock = stockRepository.findById(1L).orElseThrow();

        // 100 - (1 * 100) = 0
        assertEquals(0L, stock.getQuantity());

    }
}

 

구현이 간단하다는 장점이 있지만, SpRedis에게 부담을 줄 수 있으므로 lock획득 재시도 시 텀(위 코드에서는 thread.sleep으로 텀을 주었음)을 주어야 한다.

 

Redisson 코드

Redisson 라이브러리 설치

implementation 'org.redisson:redisson-spring-boot-starter:3.17.4'

 

redisson은 lock 관련 클래스를 제공해주기 때문에 repository는 필요없다.

하지만 lock 획득, 해제는 직접 작성해야 하므로 facade 클래스를 생성한다.

@Component
public class RedissonLockStockFacade {
    private RedissonClient redissonClient;

    private StockService stockService;

    public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) {
        this.redissonClient = redissonClient;
        this.stockService = stockService;
    }

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

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

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

            stockService.decrease(key, quantity);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}

 

Test코드

@SpringBootTest
class RedissonLockStockFacadeTest {
    @Autowired
    private RedissonLockStockFacade redissonLockStockFacade;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before() {
        Stock stock = new Stock(1L, 100L);

        stockRepository.saveAndFlush(stock);
    }

    @AfterEach
    public void after() {
        stockRepository.deleteAll();
    }


    @Test
    public void 동시에_100개의_요청() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                try {
                    redissonLockStockFacade.decrease(1L, 1L);
                } finally {
                    {
                        latch.countDown();
                    }
                }
            });
        }

        latch.await();

        Stock stock = stockRepository.findById(1L).orElseThrow();

        // 100 - (1 * 100) = 0
        assertEquals(0L, stock.getQuantity());

    }
}

 

Lettuce와 Redisson의 비교

- Lettuce

  1. 구현이 간단하다
  2. spring data redis를 이용하면 lettuce가 기본이기 때문에 별도의 라이브러리를 사용하지 않아도 된다.
  3. spin lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있다.

 

- Redisson

  1. lock 획득 재시도를 기본으로 제공한다.
  2. pub-sub방식으로 구현되어 있기 때문에 lettuce 대비 redis에 부하가 덜 간다.
  3. 별도 라이브러리를 사용해야 한다.
  4. lock을 라이브러리 차원에서 제공하기 때문에 사용법을 공부해야한다.

 

실무에서는

- 재시도가 필요하지 않은 lock은 lettuce활용

- 재시도가 필요한 경우에는 redisson 활용

 

Mysql과 Redis 비교

- Mysql

  1. 이미 Mysql을 사용하고 있다면 별도의 비용없이 사용 가능하다.
  2. 어느정도의 트래픽까지는 문제 없이 활용이 가능하다.
  3. Redis보다 성능이 좋지 않다

- Redis

  1. 활용중인 Redis가 없다면 별도의 구축비용과 인프라 관리비용이 발생한다.
  2. Mysql보다 성능이 좋다.

 

- 강의 수강 후 정리한 내용입니다.

https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard

 

재고시스템으로 알아보는 동시성이슈 해결방법 - 인프런 | 강의

동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

728x90