동시성 문제를 해결하는 여러 방법이 있지만, 여기선 Redis 클라이언트인 Redisson 분산락을 이용해서 예제를 통한 동시성을 제어하는 포스팅을 진행하겠습니다.
❌ 테스트 실패
Redis 클라이언트 중에 Redisson을 사용하면 좋은 이점을 공유합니다.
setnx
메서드를 이용해 사용자가 직접 스핀락
형태로 구성하게 됩니다. 락이 점유 시도를 실패했을 경우 계속 락 점유 시도를 하게 됩니다. 이로 인해 레디스는 계속 부하를 받게 되며, 응답시간이 지연됩니다.Distributed locks are a very useful primitive in many environments where different processes must operate with shared resources in a mutually exclusive way.
tryLockInnerAsync
메서드를 확인해보면 Lua Script를 사용해서 자체 TTL을 적용하는 것을 확인할 수 있습니다hincrby
명령어는 해당 field가 없으면 increment 값을 설정합니다.pexpire
명령어는 지정된 시간(milliseconds) 후 key 자동 삭제합니다. dependencies {
implementation 'org.redisson:redisson-spring-boot-starter:3.16.3'
}
spring:
redis:
host: localhost
port: 6379
redis:
stock:
prefix: stocks
public void decrease(final String key, final int count){
final String lockName = key + ":lock";
final RLock lock = redissonClient.getLock(lockName);
final String worker = Thread.currentThread().getName();
try {
if(!lock.tryLock(1, 3, TimeUnit.SECONDS))
return;
final int stock = currentStock(key);
if(stock <= EMPTY){
log.info("[{}] 현재 남은 재고가 없습니다. ({}개)", worker, stock);
return;
}
log.info("현재 진행중인 사람 : {} & 현재 남은 재고 : {}개", worker, stock);
setStock(key, stock - count);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock != null && lock.isLocked()) {
lock.unlock();
}
}
}
tryLock
메서드를 살펴보면,leaseTime
시간 동안 락을 점유하는 시도합니다. waitTime
시간까지 기다립니다.leaseTime
시간이 지나면 자동으로 락이 해제됩니다.waitTime
동안 락 점유를 기다리며 leaseTime
시간 이후로는 자동으로 락이 해제되기 때문에 다른 스레드도 일정 시간이 지난 후 락을 점유할 수 있습니다. @BeforeEach
void 재고_키_세팅(){
final String name = "peanut";
final String keyId = "001";
final int amount = 100;
final Stock peenut = new Stock(name, keyId, amount);
this.stockKey = stockService.keyResolver(peenut.getName(), peenut.getKeyId());
this.peenut = peenut;
stockService.setStock(this.stockKey, amount);
}
@Test
@Order(1)
void 상품_수량_확인(){
final int amount = this.peenut.getAmount();
final int currentCount = stockService.currentStock(stockKey);
assertEquals(amount, currentCount);
}
@Test
@Order(2)
void 상품_재고_카운트만큼_감소(){
final int amount = this.peenut.getAmount();
final int count = 2;
stockService.decrease(this.stockKey, count);
final int currentCount = stockService.currentStock(stockKey);
assertEquals(amount - count, currentCount);
}
상품_수량_확인
테스트에서 땅콩이 100개가 들어가있음을 확인상품_재고_카운트만큼_감소
테스트에서 땅콩 100개에서 2개를 뺀 수량을 확인 @Test
@Order(4)
void 락O_땅콩_100개를_사람_100명이_2개씩_구매() throws InterruptedException {
final int people = 100;
final int count = 2;
final int soldOut = 0;
final CountDownLatch countDownLatch = new CountDownLatch(people);
List<Thread> workers = Stream
.generate(() -> new Thread(new BuyWorker(this.stockKey, count, countDownLatch)))
.limit(people)
.collect(Collectors.toList());
workers.forEach(Thread::start);
countDownLatch.await();
final int currentCount = stockService.currentStock(this.stockKey);
assertEquals(soldOut, currentCount);
}
private class BuyWorker implements Runnable{
private String stockKey;
private int count;
private CountDownLatch countDownLatch;
public BuyWorker(String stockKey, int count, CountDownLatch countDownLatch) {
this.stockKey = stockKey;
this.count = count;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
stockService.decrease(this.stockKey, count);
countDownLatch.countDown();
}
}
요새 동시성에 대해 관심이 가던 도중, 분산 아키텍처 환경에서는 레디스로 동시성을 제어할 수 있다는 말을 듣고 '나도 한 번 구현해봐야겠다' 생각하고 레퍼런스를 찾아보고 바로 예제를 만들게 되었습니다.
하는 내내 재밌게 공부하고 다양한 레벨에서 동시성을 제어할 수 있구나 느꼈습니다.
회사에 당장 적용할 수는 없겠지만, 추후에 선착순 이벤트 등 공유 자원을 사용하는 곳에 의견을 내서 팀원들과 같이 동시성을 제어해볼 수 있을 것 같습니다.
디테일한 글 감사합니다. 많이 배우고 갑니다 :)