https://pius712.tistory.com/25
위 포스팅에서는 database 기능을 통해서 동시성 처리 예제를 만들어 보았다.
이번 포스팅에서는 redis 를 활용하여 동시성 제어를 해보려고한다.
이번 포스팅에서는 redis 자체에 대해서는 깊게 다루지 않으려고 한다.
선착순과는 다른점
참고로, 이전 포스팅과 이번 포스팅의 사례는 선착순 시스템의 경우에는 적절하지 않을 수 있다.
왜냐하면, 기본적으로 이전 포스팅에서는 spin lock + (낙관적 락, unique index table) 혹은 비관적 락을 사용하였고, 이번 포스팅에서도 spin lock, pub/sub 기반으로 구현할 것인데 이러한 방식은 락 공정성 개념에 따르면, 불공정 락이기 때문에 순서대로 처리되지 않는다.
간단하게 예를 들자면 락 공정성은 아래와 같다고 볼 수 있다.
반면, 불공정성의 경우 블락된 스레드들이 락을 획득하기 위해 경쟁을 하게 된다. 따라서, 블락된 순서와 관계 없이 락을 획득할 수 있고 특정상황에서는 기아상태 (starvation) 현상이 나타날 수 도 있다.
선착순에 대해서는 추후 다른 포스팅으로 조금 알아보려고 한다. redis 를 활용한다면 선착순처리가 가능하고,
timestamp를 통해 순위 측정가능하다. (서버마다 시간이 동기화되어있다는 가정하에)
SETNX 를 활용한 락
SETNX 명령은 특정 키에 값이 없는 경우 (Not Exist) 값을 세팅하는 명령어이다.
락 획득 과정
SETNX 를 통해서 아래와 같이 락 획득과 해제가 가능하다.
- SETNX 명령어를 통해 특정 키에 값을 세팅하게 되면 락 획득
- 키에 값을 세팅하지 못하면 락 획득 실패
- 작업 수행 후 해당 키 삭제
구현시 유의사항
참고로, 락 해제시 기존에 키에 저장된 value 를 확인 후 삭제 해야한다.
아래 포스팅에서 레디스가 싱글 인스턴스 경우 올바른 구현 파트에서 설명한 내용이다.
https://pius712.tistory.com/24
key 값만 가지고 락을 해제하는 경우, 아래와 같은 문제가 발생할 수 있다.
- client 1 이 락 획득
- client 1 의 락 타임아웃 경과
- client 2 가 락 획득
- client 1 이 락 해제
구현
@Component
class RedisLock(
private val redisTemplate: StringRedisTemplate
) {
fun lock(key: String, value: String): Boolean {
return redisTemplate.opsForValue().setIfAbsent(
key, value,
Duration.ofSeconds(5)
)!!
}
fun unlock(key: String, value: String) {
// 값을 확인 후, 제거 해줘야함.
val saved = redisTemplate.opsForValue().get(key)
if (saved == value) {
redisTemplate.delete(key)
}
}
}
@Service
class FeedingServiceV4(
private val foodRepositoryV4: FoodRepositoryV4,
private val petRepositoryV4: PetRepositoryV4,
private val redisLock: RedisLock,
) {
// redis 락
@Transactional
fun feed(
petId: Long,
) {
val uuid = UUID.randomUUID().toString()
try {
// spin lock 방식으로 락 획득
while (!redisLock.lock(petId.toString(), uuid)) {
Thread.sleep(500)
}
// 락 획득 후 로직 수행
val foodEntity = foodRepositoryV4.findByPetId(petId) ?: throw RuntimeException("Food not found")
if (foodEntity.count <= 0) return;
val petEntity = petRepositoryV4.findByIdOrNull(petId) ?: throw RuntimeException("Pet not found")
foodEntity.count -= 1
petEntity.power += 1;
// finally 를 통해 락 해제
} finally {
redisLock.unlock(petId.toString(), uuid)
}
}
}
다음 포스팅..
다음 포스팅에서는 lettuce 가 아닌 redisson 이라는 별도의 라이브러리를 통해, 락 구현을 하는 포스팅을 작성할 예정이다.
spin lock 구현시 지속적으로 redis 에 부하를 주게 되어 문제가되는데, redisson 을 활용하면 이러한 문제에서 벗어날 수 있다.
'동시성 처리' 카테고리의 다른 글
분산환경 동시성 처리 2 - redisson 활용 (0) | 2024.05.17 |
---|---|
분산환경 동시성 처리 1 - db 사용 (0) | 2024.05.17 |