Redisson 을 분산 Lock 을 위해 사용한 이유
데이터베이스

Redisson 을 분산 Lock 을 위해 사용한 이유

728x90

이전 포스팅에서 분산락에 관한 포스팅을 적었었는데

Redisson 이라는 라이브러리를 사용했는지 적어보려고 한다. 

 

일단 Spring-Boot-Starter-Redis 에서 사용하는 Library 는 Lettuce 라는 라이브러리를 사용한다. 

Lettuce 에서 Lock 을 구현할때는 스핀락 구조의 형태로 락을 많이 이용합니다.

    fun test() {
        val lockKey = "test"
        val lockTime = "3"
        val command = redisClient.connect().sync()

        try {
            // lock 을 획득하기 전까지 계속해서 Loop 를 순회
            while (!command.setnx(lockKey, lockTime)) {
                // process
            }
        } catch (e: Exception) {
            command.del(lockKey)
        }
    }

정상적으로 Lock 해제 를 못해줄시 문제 발생

스핀락은 계속해서 Lock 을 획득하기 위해 순회하기 때문에  만약 Lock 을 획득한 스레드나 프로세스가 Lock 을 정상적으로 해제해주지 못한다면 현재 스레드는 계속해서 락을 획득하려 시도하느라 어플리케이션이 중지될 것입니다.

이러한 상황을 방지하기 위해서 락에 대한 만료시간에 대한 정책이 필요하게 됩니다.

대표적으로 순회 횟수를 5회로 제한한다거나, 아니면 시간으로 제한한다거나를 택할 수 있을 겁니다.

계속해서 Redis 에 요청을 보내야 함

setnx 메소드는 만약 키가 존재하지 않는다면 설정하게 되는 것이므로 Redis 에 계속해서 LockKeyName 이 존재하는지 확인해야만 합니다. 따라서 순회하는 동안 계속해서 Redis 에 요청을 보내게 되는 것이므로 스레드 혹은 프로세스가 많다면 Redis 에 부하가 가게 될 것입니다. 

Redisson 에서 해결한 방식

Redisson 에서는 위와 같은 문제를 해결하기 위해 새로운 방법을 도입했는데요. Lettuce 에서는 Lock 에 대한 기능을 별도로 제공하지 않고, 기존 key-value 를 Setting 하는 방법과 동일하게 사용합니다. 하지만 Redisson 에서는 RLock 이라는 클래스를 따로 제공합니다.

그래서 RLock 을 이용해서 쉽게 분산락을 구현 가능한데요. 아래처럼 RLock 안에 lock() 메소드가 존재합니다.

Pub-Sub 구조

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }

lock 을 구현하는 코드를 모두는 살펴볼 수는 없고, 일단 기본적으로 위의 코드를 한번 가볍게 살펴봅시다. 위의 코드를 보면 subscribe 형태로 구성되어 있는걸 확인할 수 있습니다. 왜 subscrbie 로 구성했는지를 살펴보면 위에 봤던 SpinLock 에서는 Lock 을 취득하기 위해 계속해서 순회를 돌며 요청을 보냈었는데 Redisson 에서는 이를 pub-sub 구조를 통해 이를 개선했습니다. 따라서 이벤트기반의 Pub-Sub 구조로 진행해서 Redis 에 가는 스핀락에 관해서 부하를 줄일 수 있습니다.

Lock 만료기간

아까 위에서 설명했듯이 Lock 을 해제하는 과정 중 정상적으로 Lock 이 해제가 되지 않는다면 문제가 발생할 수 있는데요. 그래서 Redisson 에서는 LockExpire 를 설정할 수 있도록 해줍니다. 그래서 Redison 의 tryLock Method 에서는 leaseTime 을 설정할 수 있습니다.

    /**
     * Tries to acquire the lock with defined <code>leaseTime</code>.
     * Waits up to defined <code>waitTime</code> if necessary until the lock became available.
     *
     * Lock will be released automatically after defined <code>leaseTime</code> interval.
     *
     * @param waitTime the maximum time to acquire the lock
     * @param leaseTime lease time
     * @param unit time unit
     * @return <code>true</code> if lock is successfully acquired,
     *          otherwise <code>false</code> if lock is already set.
     * @throws InterruptedException - if the thread is interrupted
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

 

따라서 Lock 의 만료기간을 설정하여 위에서 설명했던 정상적으로 Lock 이 해제되지 않아 발생하는 문제를 해결할 수 있습니다.

하지만 다른 문제가 하나 발생할 수 있다는 문제 또한 존재합니다.

위의 코드를 보면 Lua Script(?) 를 이용해 Redis 자체에서 Expire 를 관리하고 있음을 확인할 수 있습니다.

pexpire 는 redis 명령어로 설정해둔 시간이 지나면 자동적으로 Key 값을 삭제하는 커맨드입니다.

Lock 경과시간 만료후 Lock 에 접근

만약 A 프로세스가 Lock 을 취득한 후 leaseTime 을 1초로 설정했다고 해봅시다.

근데 A 프로세스의 작업이 2초가 걸리는 작업이였다면 이미 Lock 은 leaseTime 이 경과하여 도중에 해제가 되었을 테고, A 프로세스는 Lock 에 대해서 Monitor 상태가 아닌데 Lock 을 해제하려고 할 것 입니다.

따라서 IllegalMonitorStateException 이 발생하게 됩니다. 

 

따라서 분산락을 적용할 경우 위와 같은 Lock 해제시 접근 하는 경우도 있게되므로 적절한 예외처리가 필요합니다.