ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동시성 문제 해결 방안
    Spring-Boot/재고시스템으로 알아보는 동시성이슈 해결방법 2023. 4. 24. 09:22

    예상

    - 스레드 1의 작업이 끝나고 스레드 2의 작업이 이루어질 것이라고 예상

    실제

    - 스레드 1이 데이터를 가져가서 갱신하기 전에 스레드 2가 데이터를 가져간다.

    - 둘 다 갱신을 하지만 quantity가 5이기에 갱신 누락 발생

     

    * 이렇게 둘 이상의 스레드가 공유 데이터에 Access할 수 있고 동시에 변경할려고 할 때 발생하는 문제점이 동시성 문제이다.

     

    1. Synchronized 이용

    // @Transactional
    public synchronized void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        
        stock.decrease(quantity);
        
        stockRepository.saveAndFlush(stock);   
    }

    - @Transactional 동작방식으로 인해 테스트 실패

        ㆍ클래스를 새로 만들어 실행

        ㆍ트랜잭션 종료시점에 데이터 업데이트를 하기 전에 메소드 호출이 가능

     

    문제점

    - 자바의 Synchronized는 하나의 프로세스 안에서만 보장된다.

     


    2. DB 이용

    Mysql을 활용한 다양한 방법

     

    1. Pessimistic Lock

    - 실제로 데이터에 Lock을 걸어서 정합성을 맞추는 방법이다.

    - exclusive lock 을 걸게되면 다른 트랜잭션에서는 lock이 해제되기 전에 데이터를 가져갈 수 없게 된다.

    - 데드락이 걸릴 수 있기 때문에 주의해야 한다.

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id=:id")
    Stock findByIdWithPessimisticLock(Long id);

     

    장점

    - 충돌이 빈번하게 일어난다면 Optimistic Lock보다 좋을 수 있다.

    - lock을 통해 업데이트를 제어하기 때문에 데이터 정합성이 어느정도 보장

     

    단점

    - 별도의 락을 잡기 때문에 성능 감소가 있을 수 있다.

     

    2. Optimistic Lock

    - 실제로 Lock을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법이다.

    - 먼저 데이터를 읽은 후에  update를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하며 업데이터 한다.

    - 내가 읽은 버전에서 수정사항이 생겼을 경우에는 application에서 다시 읽은후 작업을 수행해야 한다.

    // domain 
    @Version
    private Long version;
    
    
    // repository
    @Lock(value = LockModeType.OPTIMISTIC)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithOptimisticLock(Long id);
    
    // facade : 재시도 로직
    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (true) {
            try {
                optimisticLockStockService.decrease(id, quantity);
    
                break;
            } catch (Exception e) {
                Thread.sleep(50);
            }
        }
    }

     

    장점

    - 별도의 락을 잡지 않으므로 Pessimistic Lock 보다 성능상 이점이 있다.

     

    단점

    - 업데이트가 실패했을 때 재시도 로직을 개발자 직접 작성해야 한다.

     

    * 충돌이 비번하게 일어난다면 Pessimistic Lock / 충돌이 빈번하게 일어나지 않는다면Optimistic Lock

     

    3. Named Lock

    - 이름을 가진 metadata locking 이다.

    - 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없도록 한다.

    - 주의할 점으로는 transaction이 종료될 때 lock이 자동으로 해제되지 않는다.

    - 별도의 명령어로 해제를 수행해주거나 선점시간이 끝나야 해제된다.

    - 실제로 사용할 때는 dataSource를 분리하여 사용하는 것을 추천

        ㆍ같은 dataSource를 사용하면 커넥션 풀이 부족해지는 현상 발생

    // repository
    @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
    void getLock(String key);
    
    @Query(value = "select release_lock(:key)", nativeQuery = true)
    void releaseLock(String key);
    
    // facade
    @Transactional
    public void decrease(Long id, Long quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrease(id, quantity);
        } finally {
            lockRepository.releaseLock(id.toString());
        }
    }
    
    // service
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
    
        stock.decrease(quantity);
    
        stockRepository.saveAndFlush(stock);
    }

     

    장점

    - 주로 분산락을 사용할 때 사용, Pessimistic Lock은 타임아웃을 구현하기 힘들지만 Named Lock은 손쉽게 구현할 수 있다.

    - 데이터 삽입시에 정합성을 맞춰야할 때 사용

     

    단점

    - 트랜잭션 종료 시에 락 해제와  세션 관리를 잘 해줘야 함으로 주의해서 사용해야한다.

    - 실제로 사용할 때는 구현방법이 복잡할 수 있다.

     


    3. Redis 이용

    1. Lettuce

    - setnx 명령어를 활용하여 분산락 구현

        ㆍkey-value 가 기존의 값이 없을 때 set하는 명령어

    - spin lock 방식

        ㆍretry 로직을 개발자 작성

        ㆍ락을 획득할려는 스레드가 락을 사용할 수 있는지 반복적으로 확인하면서 락 획득을 시도하는 방식

    // 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();
        }
    }
    // facade
    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);
        }
    }

    장점

    - 구현이 간단하다.

    - spring data redis를 이용하면 lettuce가 기본이기 때문에 별도의 라이브러리를 사용하지 않아도 된다.

     

    단점

    - spin lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈  수 있다.

     

    2. Redisson

    - pub-sub 기반으로 Lock 구현 제공

        ㆍ채널을 하나 만들고 락을 점유 중인 스레드가 락 획득 대기 중인 스레드에게 해제를 알려주면

            안내를 받은 스레드가 락 획득 시도

        ㆍ별도의 retry 로직을 작성하지 않아도 된다.

    - mvnRepository - Redisson / Spring Boot Starter 의존성 추가

    // 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(10, 1, TimeUnit.SECONDS);
    
                if (!available) {
                    System.out.println("lock 획득 실패");
                    return;
                }
    
                stockService.decrease(key, quantity);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }

    장점

    - 락 획득 재시도를 기본으로 제공한다.

    - pub-sub 방식으로 구현이 되어있기 때문에 lettuce와 비교했을 때 redis에 부하가 덜 간다.

     

    단점

    - 별도의 라이브러리를 사용해야 한다.

    - lock을 라이브러리 차원에서 제공해주기 때문에 사용법을 공부해야 한다.

     

     

    실무에서는 ?

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

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


    4. Mysql 과 Redis 비교하기

    Mysql

    - 이미 Mysql을 사용하고 있다면 별도의 비용없이 사용가능하다.

    - 어느 정도의 트래픽까지는 문제없이 활용이 가능하다.

    - Redis보다는 성능이 좋지 않다.

     

    Redis

    - 활용중인 Redis가 없다면 별도의 구축비용과 인프라 관리비용이 발생한다.

    - 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

     

    github : https://github.com/sangyongchoi/stock-example

    댓글

Designed by Tistory.