본문 바로가기
동시성 처리

분산환경 동시성 처리 2 - redis SETNX 사용

by pius712 2024. 5. 17.

https://pius712.tistory.com/25

 

분산환경 동시성 처리 1 - db 사용

동시성 이슈란 무엇인가?멀티 스레딩 환경에서는 동시성 이슈로 인해서 공유자원이 경쟁상태(race condition)에 놓이는 상황이 발생할 수 있다. JVM 진영에서는 객체의 상태 변수도 race condition 이 발

pius712.tistory.com

 

위 포스팅에서는 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

 

redis lock 1부 (feat. Redlock)

분산 락이란?분산 락은 여러 개의 클라이언트가 특정 리소스에 접근할 때, 한번에 하나의 클라이언트만 접근(mutual exclusive) 할 수 있도록 하는 메커니즘이다.분산락의 조건분산 락은 최소한 아래

pius712.tistory.com

 

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 을 활용하면 이러한 문제에서 벗어날 수 있다.