2025 07 08
2025-07-08¶
Redisson MultiLock¶
참고: https://www.javadoc.io/doc/org.redisson/redisson/3.14.0/org/redisson/RedissonMultiLock.html
참고: https://www.javadoc.io/doc/org.redisson/redisson/3.14.0/org/redisson/api/RedissonClient.html
- 개요
RedissonMultiLock은 여러 노드에 대한 락을 조합하여 사용하는 RedLock 구현체- 몇가지의 독립적인 락을 하나의 락 처럼 그룹화하여 다룸
- 구현
RedissonClient:getMultiLock(RLock... locks)- 정의된 lock에 대한 MultiLock 인스턴스 반환
RedissonMultiLock:tryLockAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId)- threadId로 명시한 thread와 leaseTime으로 락을 획득하려고 함
- lock을 획득할 때 까지 waitTime 만큼 기다림
- leaseTime 인터벌 이후에 lock은 자동으로 해지함
- 락 획득 시도할 때, multiLock에 정의된 전체 내부의 모든 락을 다 획득해야 함
newRLock성공,oldRLock실패 시, 모든 락에 대해 즉시 해제 명령 날림
- 오버헤드
- 락 획득/락 해제에 상당한 오버헤드가 발생하더라... 따라서 매우 작은 임계영역에서만 사용할 것!
하나의 연산 = (락 획득 시간 + 실제 작업 시간 + 락 해제 시간)- 락 획득 시간
- Network Round-Trip Time: 두개의 Redis 서버 각각에게 락을 잡아달라는 명령. 최소 2번의 NRT
- 락 경합 및 대기: 두 번째 작업부터는 앞선 작업이 락 해제해줄 때 까지 기다림, Pub/Sub을 통한 락 해제 알림 대기
- 실제 작업 시간
- 락 해제 시간
multiLock.unlockAsync()역시 두개의 서버 각각에게 락 해제하는 명령
- 락 획득 시간
- 테스트 코드
- countDownLatch를 사용하여 모든 작업이 완료될 때 까지 쓰레드 기다리게 할 수 있음
"멀티락 테스트" should { "멀티락 10개를 동시에 수행했을 때 정상적으로 동작해야 한다." in { val taskCount = 10 val count = new AtomicInteger(0) val latch = new CountDownLatch(taskCount) // 10개의 작업 완료를 기다릴 Latch def addKey(uuid: String) = Future { try { Thread.sleep(100) println(s"addKey function called, uuid: $uuid") count.incrementAndGet() println(s"key after increment: ${count.get()}") } finally { latch.countDown() // 작업이 성공하든 실패하든 Latch 카운트를 줄임 } } for (_ <- 1 to taskCount) { val uuid = System.nanoTime() // Try로 감싸서 redisLockClient 호출 자체의 예외 발생에 대비 Try(redisLockClient.multiLockAsync(testRedisKey, uuid)(addKey(uuid.toString))) .recover { case _ => latch.countDown() } // 락 요청 자체 실패 시에도 카운트 감소 } // Latch가 0이 될 때까지 최대 30초간 대기 val allTasksCompleted = latch.await(30, TimeUnit.SECONDS) println(s"Final count: ${count.get()}") allTasksCompleted should be(true) // 타임아웃이 발생하지 않았는지 확인 count.get() should be(taskCount) } }
- countDownLatch를 사용하여 모든 작업이 완료될 때 까지 쓰레드 기다리게 할 수 있음
-
데드락 걸릴 가능성?
- 프로세스 B가 진행되기 위해, 프로세스 A가 점유하고 있는 리소스를 대기하는 상황... (여기선 Lock)
- 1차 배포: Old -> Old & New
- 시나리오 1.
- 프로세스 A Old 잡음
- 프로세스 B Old&New 잡기 위해 대기 중
- 프로세스 A Old 해제
- 프로세스 B Old&New 잡음
- 시나리오 2.
- 프로세스 A Old&New 잡음
- 프로세스 B Old 잡기 위해 대기 중
- 프로세스 A Old&New 해제
- 프로세스 B Old 잡음
- 시나리오 1.
- 2차 배포: Old&New -> New
- 시나리오 1.
- 프로세스 A Old&New 잡음
- 프로세스 B New 잡기 위해 대기 중
- 프로세스 A Old&New 해제
- 프로세스 B New 잡음
- 시나리오 2.
- 프로세스 A New 잡음
- 프로세스 B Old&New 잡기 위해 대기 중
- 프로세스 A New 해제
- 프로세스 B Old&New 잡음
- 시나리오 1.
- 유일하게 락 걸릴 가능성 (둘 다 Old&New 락 잡을 때)
- 프로세스 A Old&New 잡을라고 함 -- 프로세스 B Old&New 잡을라고 함
- 프로세스 A는 Old 부터 잡았고, 프로세스 B는 New 부터 잡았음
- 이러면 서로가 서로 잡은거 놔줘야 한명이라도 잡는데, 못잡는 교착상태 발생
- 이러려면 항상 동일한 순서로 Old&New를 잡는 보장이 있어야 함.
- 다행히 RedissonMultiLock은 명확한 순서로 잡음
- 참고: https://github.com/redisson/redisson/blob/master/redisson/src/main/java/org/redisson/RedissonMultiLock.java#L82
- 현재 코드베이스 버전은 3.14
// Redisson.java - val multiLock = oldClient.getMultiLock(oldRLock, newRLock) @Override public RLock getMultiLock(RLock... locks) { return new RedissonMultiLock(locks); } // RedissonMultiLock.java public RedissonMultiLock(RLock... locks) { if (locks.length == 0) { throw new IllegalArgumentException("Lock objects are not defined"); } this.locks.addAll(Arrays.asList(locks)); // 락 순서 리스트에 저장 } @Override public RFuture<Boolean> tryLockAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { RPromise<Boolean> result = new RedissonPromise<Boolean>(); LockState state = new LockState(waitTime, leaseTime, unit, threadId); state.tryAcquireLockAsync(locks.listIterator(), result); return result; } void tryAcquireLockAsync(ListIterator<RLock> iterator, RPromise<Boolean> result) { if (!iterator.hasNext()) { checkLeaseTimeAsync(result); return; } RLock lock = iterator.next(); RPromise<Boolean> lockAcquiredFuture = new RedissonPromise<Boolean>(); if (waitTime == -1 && leaseTime == -1) { lock.tryLockAsync(threadId) .onComplete(new TransferListener<Boolean>(lockAcquiredFuture)); } else { long awaitTime = Math.min(lockWaitTime, remainTime); lock.tryLockAsync(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS, threadId) .onComplete(new TransferListener<Boolean>(lockAcquiredFuture)); } // ... }
- 현재 코드베이스 버전은 3.14