
java의 Redis 클라이언트 중 하나인 Redisson을 활용하여 분산락을 적용
회사에서 서비스 중인 앱에 새롭게 사은품을 포인트로 구매할 수 있는 시스템이 도입되었다. 사은품에는 재고량이 정해져있었기에 멀티 스레드를 기본적으로 사용하는 Spring에서 동시성 문제가 발생할 수 있다고 생각하였다.
이를 확인하고자 코드를 모두 구현한 후 JMeter를 이용하여 부하테스트를 간단하게 진행해보았다.
테스트 조건: Thread 100개
테스트 전 재고 수

테스트 후 재고 수

위 사진을 보면 알 수 있듯이 100개의 Thread, 즉 100 명의 사용자가 동시에 주문 API를 호출했을 때 실제 재고는 26개 밖에 줄어 들지 않았다. (74% 유실)
동시성 문제가 발생하는 원인은 크게 4가지가 있다.
이 중 현재 발생하는 문제는 경합 조건(Race Condition)이 원인으로 파악된다.
(시스템이 다운되지 않았고, 재고 감소와는 달리 주문 내역은 알맞게 100건이 DB에 저장되었음)
경합 조건으로 인해 동시성 이슈가 발생하는 경우 낙관적 락, 비관적 락, 분산 락 등 다양한 방법으로 이 문제를 해결할 수 있다. 때마침 프로젝트에 Redis가 사용되고 있기 때문에 Java 진영의 Redis Client 중 하나인 Redisson을 활용하여 분산 락을 적용하여 해결하기로 결정했다.
데이터의 원자성을 확보대표적으로 Jedis, Lettuce와 Redisson 존재한다. 이 중 Jedis는 나머지 두 Client에 비해 상대적으로 부족한 성능과 분산락에 관한 래퍼런스가 없어 고려하지 않고 Lettuce와 Redisson의 장단점을 비교하여 최종적으로 Redisson을 선택하였다.
java.util.concurrent.locks.Lock 인터페이스를 구현하고 있어 분산 환경에서도 일반 Java 락처럼 사용 가능프로젝트에 이미 Redisson 관련 설정이 모두 적용돼있기에 구성 환경 설정은 개인 프로젝트에서 정리 후 재포스팅 예정
기존 코드에 RLock을 적용하여 재구현해보았다.
@Transactional(rollbackFor = Exception.class)
public Long makeOrder(Long memberId, Long itemId, OrderInfoAndDeliveryHistoryDTO.SaveRequest request) {
log.info("MAKE ORDER :: {}", itemId);
RLock lock = acquireLock(itemId);
try {
return performOrderTransaction(memberId, itemId, request);
} finally {
releaseLock(lock);
}
}
private RLock acquireLock(Long itemId) {
RLock lock = redissonClient.getLock("itemLock:" + itemId);
try {
if (!lock.tryLock(10, 30, TimeUnit.SECONDS)) {
log.error("COULD NOT ACQUIRE A LOCK FOR ITEM :: {}", itemId);
throw new RuntimeException("COULD NOT ACQUIRE A LOCK FOR ITEM :: " + itemId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("LOCK ACQUISITION INTERRUPTED", e);
throw new RuntimeException("LOCK ACQUISITION INTERRUPTED", e);
}
return lock;
}
최종적으로 아래와 같이 데이터 유실없이 재고 감소가 Thread의 수인 100만큼 알맞게 줄어든 것을 확인하였다.
분산락 테스트 전 재고수

분산락 테스트 후 재고수

결과는 대만족이었으나 찝찝함이 많이 남았다. 앞서 얘기했던 것처럼 Redis에 관한 설정은 기존에 있는 것을 그대로 사용하였고, 아직 Redisson 내부적으로 RLock이 어떤식으로 동작하는지 깊이있게 알지 못하기에 이 부분을 더 공부하여 재정리한 포스팅을 작성해보아야겠다.