동시성 이슈 해결방법
- 이번 시간에는 Redis Distributed Lock을 활용한 해결방법을 알아보자.
해결법3. Redis
Lettue
- setnex 명령어를 활용한 분산락
- setnex
- 데이터베이스에 동일한 key가 없을 경우에만 저장
- 반환값 : 1, 0
- 1 : 성공
- 0 : 실패
- setnex
- spin lock 방식으로 실패시 처리로직을 구현해야한다.
- 다른 스레드가 lock을 소유하고 있다면 그 lock이 반환될 때까지 계속 확인하며 기다리는 것
Redission
- pub-sub 기반으로 Lock 구현 제공
- 실패에 따른 처리 로직을 구현할 필요가 없다.
pub-sub인데 채널은 어떻게 정해지지?
- tryLock으로 락을 획득할 때 subscribe가 진행된다.
채널 이름은 Redis의 정보에서 뭔가 가져와서 하는 것 같은 데 정확히는 모르겠다. 좀 더 찾아봐야 할듯 !
해결법3 - 적용
1. Lettue
- 먼저 간단하게 redis-cli로 setnx을 사용해보자.
- 처음 setnx를 통해 키가 1이고 값이 lock인 값을 저장하려고 하면 키가 1인 값이 없기 때문에 저장하고 반환값으로 1을 받게 된다.
- 하지만 그 다음 다시 setnx를 통해 저장하려고 하면 이미 해당 키로 저장이 되어있기 때문에 반환값으로 0을 받으며 저장을 할 수 없게 된다.
- 이러한 방식을 통해 lock을 사용하며 MySQL의 Named Lock와 유사한 점이 있다.
- 차이점이라면 Session을 신경쓰지 않아도 되는 것이다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@Component
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {![[Untitled 11.png]]
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();
}
}
- 강의에서는 RedisTemplate를 사용했는 데 StringRedisTemplate를 사용해도 무방하다.
- 또한,
localhost:6379
라면 따로 Redis설정을 하지 않아도 Spring에서 자동적으로 잡게 된다.
- 또한,
- Lettuce 방식도 실패 로직에 대해 구현을 해줘야 하기 때문에 Facade 패턴을 적용한다.
@Component
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
this.redisLockRepository = redisLockRepository;
this.stockService = stockService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(id)) {
Thread.sleep(100); // 부하를 줄이기 위한 텀
}
try {
stockService.decrease(id, quantity);
} finally {
redisLockRepository.unlock(id);
}
}
}
- lock을 획득하는 과정에서 Redis에게 부하가 있을 수 있으니 100ms 이후에 다시 요청하도록 텀을 준다.
- 이후 테스트를 해보면 통과하는 것을 확인할 수 있다.
- Lettuce를 사용하면 구현이 간단하다는 장점이 있지만 spin lock방식이므로 Redis에 부하를 줄 수 있다.
- 그렇기 때문에 lock 획득 재시도 간에 텀을 주도록 하였다.
2. Redisson
Lettuce와 같은 자바 레디스 클라이언트이며 Netty를 사용해서, 비동기 논블록킹 I/O를 제공하는데, 특이하게도 레디스의 명령어를 직접 제공하지 않고 Lock과 같은 특정한 구현체의 형태를 제공한다.
- 먼저 간단하게 redis-cli로 pub-sub을 사용해보자.
subscribe 채널명
명령어로 구독을 하고 다른 쪽에서publish 채널명 메시지
명령어로 메시지를 보내면 구독을 한 곳에서 메시지를 받는 것을 확인할 수 있다.- redisson은 자신이 점유하던 lock을 해제할 시 채널에 메시지를 보내줌으로써 락을 획득해야하는 스레드들에게 락을 획득하라고 전달해주게 된다.
- 그러면 lock을 획득해야하는 스레드들은 메시지를 받을 때 락 획득을 시도한다.
- Lettuce는 lock획득을 계속 시도하는 반면 Redisson은 락 해제가 되었을 때나 그외 몇번만 시도하기 때문에 Redis에 부하를 줄여주게 된다.
implementation 'org.redisson:redisson-spring-boot-starter:3.17.5'
@Component
public class RedissonLockStockFacade {
private static final Logger log = LoggerFactory.getLogger(RedissonLockStockFacade.class);
private final RedissonClient redissonClient;
private final 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(5, 1, TimeUnit.SECONDS);
if (!available) {
log.info("락 획득 실패");
return;
}
stockService.decrease(key, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
- Redisson은 별도의 Repository가 필요없고 RedissonClient를 사용하면 된다.
- Lua Script를 사용해서 자체 TTL를 적용하는 것을 확인할 수 있다.
hincrby
는 해당 필드가 없으면 increment값을 설정한다.perpire
는 지정된 시간 후 key를 자동 삭제하게 된다.- 여기서는 lock을 삭제할 때 사용하는 것 같다.
tryLock(long waitTime**,** long leaseTime**,** TimeUnit unit)
- 락을 사용할 수 있을 때 까지 waitTime 시간까지 대기
- leaseTime 시간 동안 락을 점유하는 시도
- leaseTime 시간이 지나면 자동으로 락이 해제
- 즉, 선행 락 점유 스레드가 존재한다면 waitTime동안 락 점유를 기다리며 leaseTime 시간 이후로는 자동으로 락이 해제되기 때문에 다른 스레드도 일정 시간이 지난 후 락을 점유할 수 있다.
단, 장점
- pub-sub 기반으로 redis의 부하를 줄여준다는 장점이 있지만 Lettuce에 비해서 별도의 라이브러리 사용과 구현이 복잡하다는 점이 있다.
정리
- Lettuce
- 구현이 간단하며 spring-data-redis를 이용하면 Lettuce가 기본이기 때문에 별도의 라이브러리를 추가하지 않아도 된다.
- 하지만 spin lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있다.
- Redisson
- 락 획득 재시도를 기본으로 제공한다.
- pub-sub 방식으로 구현이 되어있기 때문에 Lettuce와 비교했을 때 Redis에 부하가 덜 간다.
- 하지만 별도의 라이브러리 추가가 필요하며 lock을 라이브러리 차원에서 제공하는 것이기 때문에 사용법을 공부해야 한다.
- 실무에선?
- 재시도가 필요하지 않는 lock은 Lettuce
- 재시도가 필요한 경우에는 redisson
MySQL과 Redis 간단 비교
- MySQL
- 이미 데이터베이스로 MySQL를 사용하고 있다면 별도의 비용없이 사용이 가능하다.
- 어느정도의 트래픽까지는 문제없이 활용이 가능하다.
- Redis보다는 성능이 좋지 않다.
- Redis
- 활용중인 Redis가 없다면 별도로 구축해야하기 때문에 인프라 관리비용이 발생한다.
- MySQL보다 성능이 좋다.
마무리
- 동시성을 처리하는 방법을 총 3가지 배웠다.
- Application 단
- Database 단
- Redis 단
- 각각 마다 장,단점이 달라 상황에 맞는 방황을 사용하면 될 것 같다.
- 이전부터 동시성은 어떻게 제어할 지 궁금하고 속으로 끙끙앍고 있었는 데 이런 강의를 만나게 되어서 너무 기쁘다.
- 하지만 대략적인 개념들만 파악했기에 깊이 있게 공부해봐야겠다.
- 프로젝트에서도 Redis를 사용했기 때문에 Redis도 정리해봐야 겠다.
Reference
'Back-End > 강의' 카테고리의 다른 글
[재고시스템으로 알아보는 동시성이슈 해결방법] 3. Database Lock (0) | 2022.08.28 |
---|---|
[재고시스템으로 알아보는 동시성이슈 해결방법] 2. Application Level에서의 해결법 (0) | 2022.08.26 |
[재고시스템으로 알아보는 동시성이슈 해결방법] 1. 들어가며 (1) | 2022.08.24 |
댓글