티스토리 뷰

반응형

📗 Spring Java Mysql 사용한 프로젝트로 아래 시나리오 기반으로 발생할 수 있는 멀티스레드 혹은 분산환경에서 동시성 문제를 해결하기 위한 분석 내용입니다. 


Ecommerce 프로젝트 시나리오 (주요 API)

  1. 사용자 잔고 충전 / 사용 
  2. 선착순 쿠폰 발급
  3. 상품 조회
  4. 주문 / 결제
  5. 상위 상품 조회

📌 동시성 문제란 ? 

 동일한 하나의 데이터에 두개이상의 스레드 혹인 세션에서 가변 데이터를 동시에 제어할 때 나타나는 문제로, 데이터의 정합성이 깨지는 문제를 말합니다. 

 

  • Race Condition
    • 두개 이상의 스레드가 공유 데이터에 액세스 할 수 있고, 동시에 변경하려 할 때 발생 할 수 있는 문제 
    • 같은 데이터를 동시에 변경 ( 공유된 가변 데이터 ) 하려해서 작업 중 하나가 누락될 수 있다.
  • Dead Lock
    • 트랜잭션의 교착상태.
    • 두 개 이상의 트랜잭션이 서로의 작업이 완료되기를 기다리면서 영원히 대기 상태에 빠지는 상황 
  • Dirty Read
    • 다른 트랜잭션에 의해 수정되었지만, 아직 커밋되지 않은 상태의 데이터를 읽는것 
    • 커밋하지 않은 상태의 데이터가 커밋될지, 롤백될지 알 수 없다 
  • Non-Repeatable Read
    • 하나의 트랜잭션에서 같은 키를 가진 데이터를 두 번 읽을 때, 그 결과가 다르게 나타나는 현상 
    • 첫번째 Read와 두번째 Read 사이에 다른 트랜잭션에서 값을 변경하거나 삭제하면 그 결과가 다르게 나올 수 있음.
  • Phantom Read
    • Non-Repeatable Read와 유사하지만, 여러 개의 데이터를 조회할 때 발생한다는 차이점.
    • 데이터를 두 번 이상 읽을 때, 첫 번째 쿼리 결과에 없던 유령(Phantom) 데이터가 다음 쿼리에 나타나는 현상

 

📌 ECommerce 프로젝트 내 발생 가능한 동시성 문제 제어 

    @Test
    public void 쿠폰_최대수량_30개_40명_동시발급_테스트() throws InterruptedException {
        //given
        int maxIssueCount = 30;
        int tryIssueCount = 40;
        List<CouponHistory> historyList = new ArrayList<>();
        Coupon coupon = couponRepository.save(
                new Coupon(1L, "TEST쿠폰", maxIssueCount, 10, historyList));

        ExecutorService executorService = Executors.newFixedThreadPool(tryIssueCount);
        CountDownLatch latch = new CountDownLatch(tryIssueCount);

        AtomicInteger exceptionCount = new AtomicInteger(0);

        for (int i = 1; i <= tryIssueCount; i++) {
            User user = userRepository.save(User.create("testUser" + i));

            executorService.submit(() -> {
                try {
                    couponService.issueCoupon(coupon.getId(), user.getId());
                } catch (CustomException e) {
                    exceptionCount.incrementAndGet();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();
        executorService.shutdown();

        assertThat(exceptionCount.get()).isEqualTo(tryIssueCount - maxIssueCount);
    }

쿠폰 동시발급 테스트 실패

 

   ✨ DataBase Lock을 활용한 Race Condition 해결하기

 

1. Pessimistic Lock 

데이터에 Lock을 걸어 정합성을 맞추는 방법

exclusive lock(베타적 잠금)을 걸게되면 다른 트랜잭션에서는 Lock이 해제되기 전에 데이터를 가져갈 수 없게 된다. 

자원요청에 따른 동시성 문제가 발생할 것이라고 예상하고 락을 거는 "비관적 락" 방식 

데이터에는 Lock을 가진 스레드만 접근이 가능하도록 제어하는 방법

❗️주의❗️데드락이 걸리 수 있음  

 

CouponJpaRepository

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT c FROM Coupon c where c.couponId = :couponId")
    Coupon findByHistoryIdWithLock(Long couponId);

 

📙 Pessimistic Lock 의 장점

  1. 충돌이 빈번하게 일어난다면 롤백의 횟수를 줄일 수 있기 때문에, Optimistic Lock 보다는 성능이 좋을 수 있다.

📘 Pessimistic Lock 의 단점

  1. 데이터 자체에 별도의 락을 잡기때문에 동시성이 떨어져 성능저하가 발생할 수 있습니다.
  2. 특히 읽기가 많이 이루어지는 데이터베이스의 경우에는 손해가 더 크다.
  3. 서로 자원이 필요한 경우, 락이 걸려있으므로 데드락이 일어날 가능성이 있습니다.

 

2. Optimistic Lock

실제 Lock을 이용하지 않고 "버전" 사용으로 정합성을 맞추는 방법

데이터를 읽은 후에 update를 수행할 때 내가 읽은 버전이 맞는지 확인해서 업데이트 하는 방식 

자원에 Lock을 걸어 선점하지 않고, 동시성 문제가 발생하면 처리하는 "낙관적 락" 방식 

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

 

Coupon(Entity)

@Version
private Long version;

CouponJpaRepository

    @Lock(value = LockModeType.OPTIMISTIC)
    @Query("SELECT c FROM Coupon c where c.couponId = :couponId")
    Coupon findByCouponIdWithOptimisticLock(Long couponId);

 

📙 Optimistic Lock 의 장점

  1. 충돌이 안나다는 가정하에, Pessimistic Lock 보다는 성능이 좋을 수 있다.

📘 Optimistic Lock 의 단점

  1. 업데이트가 실패했을 때 재시도를 개발자가 직접 처리 해줘야 한다.
  2. 충돌이 빈번하게 일어나게 되면, 롤백처리로 인해 Pessimistic Lock이 성능이 더 좋을 수 있다.

 

3. Named Lock

이름을 가진 metadata locking 

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

❗️주의❗️transaction이 종료될때 Lock이 자동으로 해제되지 않음

별도의 명령어로 해제하거나 선점시간이 끝나야 해제 

 

💡 Named Lock은 Passimistic Lock과 유사하지만,

Passimistic Lock은 row나 table 단위로 Lock

Named Lock은 metadata 단위로 Lock

 

public interface CouponJpaRepository extends JpaRepository<Coupon, Long> {
    @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
    void getLock(String key);

    @Query(value = "select release_lock(:key, key)", nativeQuery = true)
    void releaseLock(String key);
}
@Component
@RequiredArgsConstructor
public class NamedLockFacade {

    private final CouponRepository couponRepository;
    private final CouponService couponService;

    //부모의 트랜잭션과 별도로 실행되어야 함
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void issue(final Long userId, final Long couponId) {
        try {
            couponRepository.getLock(couponId.toString());
            couponService.issue(userId, couponId);
        }finally {
            //락의 해제 - 락이 해제되기 전에 커밋되도록 함
            couponRepository.releaseLock(couponId.toString());
        }
    }
}

 

📙 Named Lock 의 장점

  1. Pessimisitic락에 비해 time out을 구현하기 쉽다고 함.

📘 Named Lock 의 단점

  1. 실제 사용할 때는 구현방법이 복잡할 수 있다고 함. 

 

✨Redis 이용해보기

 

Redis를 사용하여 동시성 문제를 해결하는 대표적인 라이브러리

Lettuce 와 Redisson

 

1. Lettuce

Setnx 명령어를 활용해 분산락을 구현 

Set if not Exist - key:value를 Set할때 기존의 값이 없을 때만 Set

Setnx는 Spin Lock방식으로 retry 로직을 개발자가 작성해야함

Spin Lock이란 ? Lock을 획득하려는 스레드가 Lock을 획득할 수 있는지 확인하면서 반복적으로 시도하는 방법

 

📙 Lettuce 의 장점

  1. Mysql의 NamedLock과 유사하지만, redis 의존성을 추가하는 경우 기본 Redis Client로 제공되므로, 별도의 설정 없이 간단히 구현할 수 있다.
  2. Lettuce Connection Pool의 개수는 TPS가 높아지고 Redis 서버의 응답이 느려질 때 중요한 의미를 가진다고 함. 
  3. 세션 관리에 신경쓰지 않아도 됨. 
더보기

Redis Lettuce를 활용한다면 세션관리에 신경쓰지 않아도 되는 이유에 대해,

기존 세션 관리의 문제점

1. 서버 메모리 부담

- 각 WAS(Web Application Server)가 세션을 개별적으로 관리

- 사용자가 많아질수록 메모리 사용량 증가

- 서버 확장 시 세션 동기화 문제 발생

2. 세션 동기화 이슈

[사용자] -> [로드밸런서] -> [서버1] (세션 있음)

                                       -> [서버2] (세션 없음)

Redis Lettuce를 사용하면

1. 중앙 집중식 세션 저장소

- 모든 서버가 동일한 Redis 서버를 바라봄

- 세션 정보가 중앙에서 관리되어 동기화 불필요

2. 자동 세션 관리

3. 고가용성과 확장성

- Redis의 클러스터링/레플리케이션 지원

- 서버 확장 시에도 추가 설정 불필요

 

Redis Lettuce를 사용하면 세션 관리에 대한 많은 부분을 자동화할 수 있고, 개발자가 직접 신경 쓸 부분이 줄어들게 됩니다.

 

📘 Lettuce 의 단점

  1. Spin Lock 방식이 Lock을 얻을 때 까지 계속 시도하기 때문에 Redis에 접근해 부하를 줄 수 있다. 
  2. Lock의 time out 도 구현되어 있지않아 무한루프에 빠질 가능성도 있다. 

 

2. Redisson

Pub-Sub 기반으로 Lock 구현 제공 

Pub-Sub 방식이란 ? 채널을 하나 만들고 Lock을 점유중인 스레드가 Lock을 해제하면 대기 중인 스레드에게 알려주고 대기중인 스레드가 Lock점유를 시도하는 방식

 

📙 Redisson 의 장점

  1. Lettuce와 다르게 Retry방식을 작성하기 않아도 된다.
  2. 계속 락획득 시도를 하는게 아니기 때문에 Lettuce에 비해 Redis 부하를 줄일 수 있다.

📘 Redisson 의 단점

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

💡 Lettuce vs Redisson💡 

  Lettuce Redisson
분산 락 기능제공 직접 구현 필요 분산 락 기능 제공
라이브러리 크기 상대적으로 작음 상대적으로 큼 
구현 방식 spin lock pub-sub 구조

 

 

 


참고 

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

 

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

최상용 | 동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., 동시성 이슈 처리도 자신있게! 간단한 재고 시스템으로 차근차근 배워보세요. 백엔드 개발자라면 꼭 알아야 할 동

www.inflearn.com

https://cheese10yun.github.io/redis-lettuce-connection/#:~:text=Redis%20Lettuce%20Connection%EC%9D%80%20%EB%B9%84%EB%8F%99%EA%B8%B0,%EC%A0%90%EC%97%90%EC%84%9C%20%ED%81%B0%20%EC%9E%A5%EC%A0%90%EC%9E%85%EB%8B%88%EB%8B%A4.

 

Hikari와 비교하며 알아보는 Redis Lettuce 커넥션 풀의 특징 - Yun Blog | 기술 블로그

Hikari와 비교하며 알아보는 Redis Lettuce 커넥션 풀의 특징 - Yun Blog | 기술 블로그

cheese10yun.github.io

 

반응형

'개발' 카테고리의 다른 글

[ Kafka ] kafka란? docker, Spring 연동  (1) 2025.02.18
[Cache] 캐시란 ?  (0) 2025.01.31
[Springboot 게시판] postgreSql brew 다운로드  (0) 2024.12.03
[Springboot JPA 게시판] 프로젝트 생성  (0) 2024.12.02
ERD란 ?  (0) 2024.11.27