안녕세계

[Redis] 분산 락과 상태 키를 사용한 동시성 이슈 제어 본문

[Redis] 분산 락과 상태 키를 사용한 동시성 이슈 제어

Junhong Kim 2024. 2. 4. 19:37
728x90
반응형

이전 포스팅에서는 Redisson을 사용하여 Redis 분산 락을 손 쉽게 구현할 수 있는 방법에 대해서 알아보았습니다. 이번 포스팅에서는 특정 요구 사항을 분산 락과 상태 키를 사용하여 동시성 이슈를 제어하며 동시에 작업을 처리할 수 있는 방법에 대해 알아보겠습니다.

일반적인 분산 락 사용

분산 락을 활용하는 일반적인 방식은 먼저 락을 획득한 다음, 특정 작업(여기서는 compute 메서드)을 실행하고 나서 락을 해제하는 순서로 진행됩니다. 동시성 문제가 발생할 수 있는 작업에 분산 락을 적용하면, 동시 작업시 발생할 수 있는 문제들을 안정적으로 작업을 처리할 수 있습니다. 아래 코드는 lockKey에 대해 락을 확득하고, 로직을 실행한 뒤 락을 해제하는 분산 락 사용의 일반적인 예시입니다.

class RedissonUtil(
    private val redissonClient: RedissonClient
) {
    fun <T> lock(
        lockKey: String,
        waitTime: Long = 5,
        leaseTime: Long = 10,
        task: Callable<T>
    ): T {
        val lock = redissonClient.getLock(lockKey)
        try {
            if (lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) { // 락 획득
                println("Lock acquired")
                return task.call() // 로직 수행
            }
            error("Unable to acquire lock")
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        } finally {
            if (lock.isLocked && lock.isHeldByCurrentThread) {
                println("Lock released")
                lock.unlock() // 락 해제
            }
        }
    }
}

fun main(args: Array<String>) {
    val config: Config = Config()
    config
        .useSingleServer()
        .setAddress("redis://localhost:6379")

    val redissonClient = Redisson.create(config)
    val redissonUtil = RedissonUtil(redissonClient)

    val lockKey = "LOCK_KEY:1"

    // 일반적인 분산락 사용 예
    redissonUtil.lock(lockKey) {
        compute()
    }
}

요구 사항

서비스 요구 사항으로 compute 메서드가 수행되기 위해서는 lockKey에 해당하는 데이터(여기서는 데이터1)의 상태에 따라서 compute 로직을 수행할지 여부를 결정해야 한다고 가정합니다. 데이터1의 상태는 대기중(WAITING)으로 시작하며 처리중(PROCESSING) 또는 취소중(CANCELLING) 상태로 변경될 수 있습니다. 이때 작업자A의 처리 요청에 의해서 데이터1이 대기중에서 처리 중으로 변경된 이후, 작업자B도 데이터1에 처리 요청을 하면 함께 처리될 수 있지만, 작업자C가 처리 요청 중인 데이터1에 대하여 취소 요청을 하면 현재는 처리 중 상태이기 때문에 처리 중에는 취소 할 수 없다라는 요구사항이 있다고 가정해봅시다. (반대로 취소 상태에 처리 요청이 오면 거부되어야 함)

 

 

이처럼 분산 시스템 환경에서 특정 lockKey(여기서는 데이터1)의 상태에 따라 수행 되어야하는 로직이 달라야한다면 어떠한 방법을 사용할 수 있을까요? 이러한 요구 사항에서는 lockKey와 별도로 statusKey를 관리하여 상태에 따라 로직을 동시 수행할 수 있도록 하는 방법을 고려해볼 수 있습니다.

분산 락과 상태 키 사용

앞서 말한 것 처럼 분산 락을 걸더라도 동일한 상태에서는 동시에 수행될 수 있도록 고려해야합니다. 데이터1의 분산 락키(LOCK_KEY:1)와 별도로 상태 키(STATUS_KEY:1)에 대한 값을 관리하고, 현재 상태가 요청 상태와 동일한 상태인 경우에만 로직을 수행할 수 있도록 합니다.

 

분산 락 내부에서 상태를 확인하는 작업 compute 메서드를 수행 하면 compute 로직의 수행시간 만큼 총 처리 시간이 소요됩니다. 왜냐하면, 수행해야하는 로직이 수 초가 걸리는 작업이라면 N개의 요청이 들어왔을 때 분산 락 획득 후 각 로직을 수행해야하는 동안 다른 스레드는 대기해야 하므로 `요청 수 x 수행 시간` 만큼의 시간이 걸려서 총 처리 시간이 증가됩니다.

 

따라서, 동시에 요청이 들어올 경우 분산 락 획득을 위해서 대기하되 분산 락 안에서는 상태만 저장하고 compute 로직은 분산 락 밖에서 수행할 수 있도록 합니다.

fun main(args: Array<String>) {
    ...

    // 락 획득
    redissonUtil.lock(lockKey, 5, 10) {
        val statusKey = "STATUS_KEY:1"
        // 현재 상태키 확인
        val status = redisTemplate.opsForValue()[statusKey]?.let { Status.valueOf(it.toString()) }
        when (status) {
            null, Status.PROCESSING -> {
                // 상태키 설정 & 갱신
                redisTemplate.opsForValue().set(statusKey, Status.CANCELED.name, 30, TimeUnit.SECONDS)
            }

            Status.CANCELLING -> {
                throw RuntimeException()
            }
    }
    // 동시 실행가능한 로직
    compute()
}

위 코드는 처리 요청을 받은 스레드가 분산 락을 획득한 뒤 내부에서 상태 키를 확인하여, 취소중(CANCELLING) 상태면 RuntimeException을 발생시키고, 처리중(PROCESSING) 또는 대기 상태면(null) 상태 키를 설정하고 만료시간을 갱신합니다. 상태 키를 설정하는 로직은 복잡한 로직 없이 락을 획득한 뒤 상태 키만 갱신하고 분산 락을 해제하여 락 획득/해제만 빠르게 수행하고 동시 실행이 필요한 로직(compute 메서드)이 실행됩니다.

 

따라서, 동시에 N개 요청이 들어오더라도 각 요청들은 상태 키를 설정하는 시간 만큼만 아주 짧은 대기를 하고 락이 해제되었을 때 락을 즉시 획득할 수 있습니다. 위 처럼 코드를 보면 분산 락키에 대해 waitTime(상태 키를 설정하는 시간 이상의 시간이면 됩니다)이 걸려있기 때문에 일시적으로 대기한 뒤 락을 획득한 스레드는 현재 상태와 동일하면 상태 키를 갱신하고 compute 메서드를 실행하고, 현재 상태와 다르면 예외를 발생시켜서 compute 메서드를 수행하지 않습니다.

 

이러한 방법으로 분산 락과 상태 키를 혼용하여 사용하게되면 동일한 분산락 키에 대해서 상태가 동일한 경우에는 동시작업을 수행할 수 있도록 구현하고 상태가 다른 경우 로직을 수행하지 않도록 처리할 수 있습니다. 분산 락키는 동일하지만 상태 값에 따라 로직이 수행 여부가 결정되어야 하는 경우 분산 락키와 상태 키를 함께 사용해서 동일한 상태의 요청들은 동시에 작업을 처리하도록 구현할 수 있습니다.

728x90
반응형

'Database > Redis' 카테고리의 다른 글

[Redis] 분산 락은 어떻게 걸어야할까?  (0) 2024.02.18
[Redis] 분산 락 (feat. Redisson)  (0) 2024.01.21
Comments