SpringBoot

락 선택 이유와 성능 테스트

코카멍멍 2024. 4. 30. 18:23
반응형

테스트 종류

1. 락 X 트랜잭션 사용

2. Beta Lock 사용

3. Redisson 분산락 사용

총 3가지의 테스트를 했습니다. 각각의 코드에서 발생할 수 있는 문제점, 걸린 시간, 특징에 대해서 작성했습니다.

테스트 환경

DB: H2
Mode: MariaDB
테스트 환경: Local
Reids: Embedded
요청 스레드 수 : 32
요청 횟수 : 각 1회

Transaction을 사용하여 동시성 테스트 결과

코드 본문

@Transactional  
public void notUseLockTest(String lockName, Integer userId) {  
    BeanPay beanPay = getBeanPay(1, Role.USER);  

    final BeanPayDetail beanPayDetail = BeanPayDetail.ofCreate(  
       beanPay,  
       1,  
       5000  
    );  
    final BeanPayDetail createBeanPayDetail = beanPayDetailRepository.save(  
       beanPayDetail  
    );  
    beanPay.chargeBeanPayDetail(createBeanPayDetail.getAmount());  
}

테스트 코드

@Test  
void 트랜잭션사용_LostUpdate_발생() throws InterruptedException {  
    //given  
    Long startTime = System.currentTimeMillis();  
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);  
    CountDownLatch latch = new CountDownLatch(threadCount);  
    String lockName = "BEANPAY";  
    Integer userId = 1;  
    Integer totalAmount = threadCount * 5000;  

    //when  
    for(int i = 0; i < threadCount; i++) {  
       executorService.submit(() -> {  
          // 분산락 적용 메소드 호출  
          try {  
             lockTestService.notUseLockTest(lockName, userId);  
          }finally {  
             latch.countDown();  
          }  
       });  
    }  

    latch.await();  
    //then  
    Long endTime = System.currentTimeMillis();  
    BeanPay beanPay = beanPayRepository.findById(beanPayId).get();  
    log.info("Actual total amount : {}", beanPay.getAmount());  
    log.info("expect total amount : {}", totalAmount);  
    log.info("total Time : {}", (endTime - startTime) + "ms");  
    assertNotEquals(totalAmount, beanPay.getAmount());  

}

결과

원래는 16만원의 금액이 충전되었지만 MySQL의 기본격리수준인 Repeatable read 격리수준에서 Lock을 사용하지 않아 Lost Update가 발생하여 낮은 정합성을 보입니다.

Beta Lock을 사용한 결과

코드 본문

@Transactional  
public void betaLockTest(String lockName, Integer userId) {  
    BeanPay beanPay = beanPayRepository.findBeanPayByUserIdAndRoleUseBetaLock(1,  
       Role.USER);  

    final BeanPayDetail beanPayDetail = BeanPayDetail.ofCreate(  
       beanPay,  
       1,  
       5000  
    );  

    final BeanPayDetail createBeanPayDetail = beanPayDetailRepository.save(  
       beanPayDetail  
    );  
    beanPay.chargeBeanPayDetail(createBeanPayDetail.getAmount());  
}

테스트 코드

@Test  
void 베타락사용_성공() throws InterruptedException {  
    //given  
    Long startTime = System.currentTimeMillis();  
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);  
    CountDownLatch latch = new CountDownLatch(threadCount);  
    String lockName = "BEANPAY";  
    Integer userId = 1;  
    Integer totalAmount = threadCount * 5000;  

    //when  
    for(int i = 0; i < threadCount; i++) {  
       executorService.submit(() -> {  
          // 분산락 적용 메소드 호출  
          try {  
             lockTestService.betaLockTest(lockName, userId);  
          }finally {  
             latch.countDown();  
          }  
       });  
    }  

    latch.await();  
    //then  
    Long endTime = System.currentTimeMillis();  
    BeanPay beanPay = beanPayRepository.findById(beanPayId).get();  
    log.info("Actual total amount : {}", beanPay.getAmount());  
    log.info("expect total amount : {}", totalAmount);  
    log.info("total Time : {}", (endTime - startTime) + "ms");  
    assertEquals(totalAmount, beanPay.getAmount());  

}

결과

BetaLock은 Lock을 걸지 않는 Mvcc 모드에서 Lost Update문제를 해결할 수 있도록 데이터베이스에서 제공하는 락입니다. 해당 락은 단일 서버에서는 최적화 되어있어 높은 성능을 보이지만 Lock을 걸었을 때 읽기에 대해서 허용하지 않기 떄문에 읽기 성능을 낮출 수 있습니다.

Redisson 분산락을 사용한 결과

코드 본문

@DistributedLock(key = "#lockName.concat('-').concat(#userId)")  
public void useDistributeLock(String lockName, Integer userId) {  
    BeanPay beanPay = getBeanPay(1, Role.USER);  

    final BeanPayDetail beanPayDetail = BeanPayDetail.ofCreate(  
       beanPay,  
       1,  
       5000  
    );  

    final BeanPayDetail createBeanPayDetail = beanPayDetailRepository.save(  
       beanPayDetail  
    );  
    beanPay.chargeBeanPayDetail(createBeanPayDetail.getAmount());  
}

테스트 코드

@Test  
void 분산락사용_성공() throws InterruptedException {  
    //given  
    Long startTime = System.currentTimeMillis();  
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);  
    CountDownLatch latch = new CountDownLatch(threadCount);  
    String lockName = "BEANPAY";  
    Integer userId = 1;  
    Integer totalAmount = threadCount * 5000;  

    //when  
    for(int i = 0; i < threadCount; i++) {  
       executorService.submit(() -> {  
          //    분산락 적용 메소드 호출  
          try {  
             lockTestService.useDistributeLock(lockName, userId);  
          }finally {  
             latch.countDown();  
          }  
       });  
    }  

    latch.await();  
    //then  
    Long endTime = System.currentTimeMillis();  
    BeanPay beanPay = beanPayRepository.findById(beanPayId).get();  
    log.info("Actual total amount : {}", beanPay.getAmount());  
    log.info("expect total amount : {}", totalAmount);  
    log.info("total Time : {}", (endTime - startTime) + "ms");  
    assertEquals(totalAmount, beanPay.getAmount());  
}

결과

Redisson 분산락을 통해 높은 정합성을 보장합니다. 하지만 sub/pub 구조로 깨어나서 Redis에 접근하는 순서를 보장하지 않습니다. 하지만 높은 정합성을 보장하며 확장성이 좋다는 장점을 가지고 있습니다. 

베타락은 Lock을 거는 동안 조회를 하지 못하지만 Redisson을 사용하면 Lock과 DB가 분리되어 있어 조회가 가능합니다.

정리

Repeatable Read

  • 동시성을 보장하지 못함
  • 빠른 처리 성능을 가짐

베타락

  • 동시성을 보장함
  • 단일 데이터베이스에서 사용하기 적절함
  • 사용성 매우 좋음
  • 빠른 처리 성능을 가짐

레디슨

  • 동시성을 보장함
  • 분산 환경의 데이터베이스에서 사용하기 적절함
  • 사용성 좋음

실제환경 테스트

In-Memory 환경의 데이터베이스이기 때문에 Beta Lock의 성능이 좋게 나왔습니다. 그래서 아래는 S3 환경의 DB로 테스트 했을 때 결과입니다.

Beta Lock

Redisson Lock

실제 환경에서는 In-Memory 환경에 비해 조금 비율이 줄어든것을 알 수 있습니다.

반응형