[Redis] Redisson TryLock 동작과정 살펴보기
어플리케이션을 개발하다보면 Redis 를 이용해 DistributedLock 을 구현하다보면 Redisson 라이브러리를 많이 사용하는데요.
Redisson 은 pub/sub, lua script 를 이용한다와 같은 특징이 있습니다.
이번에는 좀 더 자세히 락을 획득하는 tryLock 메소드에 대해 어떻게 동작하는지
그리고 위 특징들이 어떻게 동작하는지 한번 살펴보려 합니다.
DistributedLock 이 어떤건지 잘 모르시겠다면 제가 작성한 아래 글을 참고해보시는것을 추천드립니다.
Redisson tryLock 사용방법
우선 tryLock
메소드를 살펴보기 전에 어떻게 사용하는지를 간략히 살펴보겠습니다.
RLock.java
public interface RLock extends Lock, RLockAsync {
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
}
tryLock
메소드는 Redisson 에서 제공하는 RLock
인터페이스에 선언되어있습니다.
그리고 RLock
는 Lock, RLockAsync 를 상속받고 있습니다.
기본 메커니즘은 Java 의 Lock 인터페이스를 확장하는 구조입니다.
- waitTime: Lock 획득을 기다리는 시간 (정의된 waitTime 동안 Lock 획득을 시도)
- leaseTime: Lock 의 대여 시간 (락 획득 후 leaseTime 만큼 지나면 Lock 을 해제)
- unit: 시간 단위
public void testMethod() {
RLock lock = redissonClient.getLock(LOCK_NAME); // (1)
try {
boolean available = lock.tryLock(10, 5, TimeUnit.SECONDS); // (2)
if (available) { // (3)
// business logic ...
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock(); // (4)
}
}
tryLock
을 사용할때는 일반적으로 다음과 같이 처리하고 있습니다.
- (1): Lock 의 이름으로 RLock 객체를 가져옵니다.
- (2): tryLock 메소드를 호출해 락의 획득여부를 가져옵니다.
- (3): 락 획득에 성공하면 이후 로직을 진행합니다.
- (4): 마지막으로 락을 해제합니다.
Redisson TryLock 내부 동작
Redisson 3.19.1 버전이며 RLock 의 구현체인 RedissonLock 을 기준으로 살펴보겠습니다.
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + time + "ms. " +
"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException e) {
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
위 코드는 RedissonLock
의 tryLock
메소드의 전체코드입니다.
다음 코드를 하나씩 살펴보며 따라가보겠습니다.
1. 락의 획득을 시도한다.
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// ...
락 획득을 위해 tryLock
메서드를 진입하면 바로 시간, 스레드Id 에 대해 변수로 만들고tryAcquire(waitTime, leaseTime, unit, threadId)
메소드에 진입합니다.
tryAcquire
메서드를 따라 들어가보면 tryAcquireAsync
메서드가 나옵니다.
tryAcquireAsync Method
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime > 0) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired
if (ttlRemaining == null) {
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
위 코드는 tryAcquireAsync
메서드 입니다.
tryAcquireAsync
내부에서 tryLockInnerAsync
메서드를 호출해 ttlRemainingFuture 를 받아옵니다.
ttlRemainingFuture 는 Lock 의 TTL 을 반환하는데요. tryLockInnerAsync
내부를 살펴보겠습니다.
tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " + // (1)
"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " + // (2)
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // (3)
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // (4)
"return nil; " + // (5)
"end; " +
"return redis.call('pttl', KEYS[1]);", // (6)
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
tryLockInnerAsync
에서 Lua Script 를 통해 레디스에 커맨드를 호출하는것을 볼 수 있습니다.
다음 스크립트를 설명드리겠습니다.
(1): redis.call('exists', KEYS[1]) == 0)
- 키가 존재하지 않는지 확인. 즉, 해당 잠금이 현재 없는지 확인합니다.
(2): redis.call('hexists', KEYS[1], ARGV[2]) == 1)
- 키가 존재하고, threadId 가 해당 키의 필드로 존재하는지 확인합니다. 이는 잠금의 재진입을 허용합니다.
(3): redis.call('hincrby', KEYS[1], ARGV[2], 1)
- KEYS[1] 해시의 ARGV[2] 필드의 값을 1 증가시킵니다. 이는 잠금을 설정하거나 재진입 시 횟수를 증가시킵니다.
(4): redis.call('pexpire', KEYS[1], ARGV[1])
- KEYS[1] 키의 TTL을 ARGV[1] 로 설정합니다. 즉, 키에 대해 TTL 을 설정합니다.
- 이때 TTL 은 전달받은 leaseTime 으로 설정됩니다.
(5): return nil
- null 을 리턴합니다.
(6): return redis.call('pttl', KEYS[1])
- KEYS[1] 의 TTL 을 리턴합니다.
요약하면 (1), (2) 둘 중 하나가 참이라면 락을 획득할 수 있다는 뜻입니다.
그렇기에 해시에 키와 TTL 을 설정하고 null 을 리턴합니다.
만약 이미 락이 선점되었다면 락을 획득할 수 없으니 해당 키의 TTL 을 리턴합니다.
즉, tryLockInnerAsync
의 반환된 TTL 이 null 이라면 락 획득에 성공한것이고 TTL 이 존재한다면 락 획득에 실패한것입니다.
이때 Redis 에 생성된 Lock 은 어플리케이션에서 설정한 Key 이름으로 설정됩니다.
그리고 Hash 자료구조로 설정되고 TTL 도 설정된것을 확인할 수 있습니다.
추가로 해당 Lock 은 Lock 을 점유한 스레드에 대한 LockName 과 count 정보도 함께 가지고 있는것을 확인할 수 있습니다.
이때, 스레드에 대한 LockName 은 Id + threadId 의 조합으로 설정됩니다.
Id 는 Redisson ConnectionManager 의 Id 입니다.
그럼 다시 tryAcquireAsync 로 돌아와서 tryLockInnerAsync 메서드를 호출하는 부분을 보겠습니다.
// ...
RFuture<Long> ttlRemainingFuture;
if (leaseTime > 0) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
// ...
앞서 TTL 은 leaseTime 으로 설정된다고 했었는데요.
전달받은 leaseTime 이 존재하면(0보다 크면) 락 획득에 대해 leaseTiem 을 전달하고
그렇지 않다면 internalLockLeaseTime 을 전달합니다.
internalLockLeaseTime 은 Redisson Config 의 lockWatchdogTimeout 이며 기본값은 30초입니다.
즉, Lock 의 TTL 이 30초로 설정된다는 뜻입니다.
그리고 tryLockInnerAsync 에서 TTL 을 받아온 후 로직입니다.
// ...
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired
if (ttlRemaining == null) {
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
// ...
TTL 이 null 이고 설정한 leaseTime 이 0보다 크다면 internalLockLeaseTime 을 leaseTime 으로 설정하는것을 볼 수 있습니다.
만약 설정한 leaseTime 이 0보다 작다면 scheduleExpirationRenewal(threadId);
을 호출합니다.
scheduleExpirationRenewal(threadId);
의 역할은 간략히 말씀드리면 Lock 을 획득할때 leaseTime 이 없어 기본 internalLockLeaseTime 으로 설정된 이후 Lock 해제시까지 TTL 을 자동으로 갱신시켜주는 역할을 합니다.
그리고 다시 tryLock 메서드입니다.
// ...
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// ...
여기서 tryAcquire
메서드로부터 TTL 을 가져와 null 인지 확인하고 null 이라면 Lock 획득에 성공했으니 true 를 리턴합니다.
이후 time 을 재계산하는데요, 처음 설정한 waitTime 에서 TTL 획득하기까지 걸린 시간을 차감합니다.
그리고 time 이 0보다 작다는것은 waitTime 이 만료되었다는 뜻이니 false 를 리턴합니다.
2. Lock에 대해 Redis Pub/Sub 채널 구독
// ...
current = System.currentTimeMillis();
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + time + "ms. " +
"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException e) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// ...
응답받은 TTL 이 존재하는 경우에는 threadId 로 채널을 구독하게 됩니다.
그리고 CompletableFuture
로 응답을 가져오고 이때 TimeoutException, ExecutionException
을 예외로 선언해 해당 예외가 발생하면 락 획득에 대해 실패로 처리하게 됩니다.
예외가 발생하는 경우는 아래와 같습니다.
TimeoutException
: 채널을 구독하여 대기하는 동안 waitTime 이 지난 경우ExecutionException
:CompletableFuture
에서 예외가 발생한 경우
이제 락을 구독하는 subscribe 메서드를 한번 살펴보겠습니다.
아래는 subscribe 메서드의 전체 코드입니다.
subscribe method
public CompletableFuture<E> subscribe(String entryName, String channelName) {
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
CompletableFuture<E> newPromise = new CompletableFuture<>();
semaphore.acquire().thenAccept(c -> {
if (newPromise.isDone()) {
semaphore.release();
return;
}
E entry = entries.get(entryName);
if (entry != null) {
entry.acquire();
semaphore.release();
entry.getPromise().whenComplete((r, e) -> {
if (e != null) {
newPromise.completeExceptionally(e);
return;
}
newPromise.complete(r);
});
return;
}
E value = createEntry(newPromise);
value.acquire();
E oldValue = entries.putIfAbsent(entryName, value);
if (oldValue != null) {
oldValue.acquire();
semaphore.release();
oldValue.getPromise().whenComplete((r, e) -> {
if (e != null) {
newPromise.completeExceptionally(e);
return;
}
newPromise.complete(r);
});
return;
}
RedisPubSubListener<Object> listener = createListener(channelName, value);
CompletableFuture<PubSubConnectionEntry> s = service.subscribeNoTimeout(LongCodec.INSTANCE, channelName, semaphore, listener);
newPromise.whenComplete((r, e) -> {
if (e != null) {
s.completeExceptionally(e);
}
});
s.whenComplete((r, e) -> {
if (e != null) {
entries.remove(entryName);
value.getPromise().completeExceptionally(e);
return;
}
value.getPromise().complete(value);
});
});
return newPromise;
}
우선 특정 채널에 대한 세마포어 와 newPromise 라는 CompletableFuture 를 선언합니다.
이후 세마포어에선 acquire 시 실행할 콜백 함수를 선언합니다.
그리고 아래에선 채널에 대한 Redis 채널에 대한 Pub/Sub Listener 를 생성하고
subscribeNoTimeout 를 통해 해당 채널을 구독합니다.
이후 newPromise 와 CompletableFuture 에 대해 콜백함수를 선언합니다.
마지막으로 CompletableFuture 에 RedissonLockEntry 을 담아 리턴합니다.
이때 레디스 채널명은 아래와 같이 channelName + Lock Key 조합으로 생성되는것을 확인할 수 있습니다.
RedissonLock.java
Redis CLI
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
이후 CompletableFuture 의 get 을 통해 값을 가져옵니다. 이때 RedissonLockEntry 객체에 세마포어를 가져오게 됩니다.
아래 디버깅 이미지를 보면 latch 에는 세마포어 객체가 존재하는것을 확인할 수 있고 counter 는 Lock을 관리하는 객체인 RedissonLockEntry 에 acquire() 한 횟수를 나타냅니다.
3. 락 획득 재시도
// ...
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
// ...
위 코드는 Redis 채널 구독 이후 로직입니다.
여기에서는 락 획득을 위해 재시도하는 로직이 진행됩니다.
하나씩 따라가보겠습니다.
// ...
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// ...
우선 waitTime 인 time 변수에 시간을 차감하여 현재 남은시간을 다시 확인합니다.time <= 0
이라는건 waitTime 이 모두 지났다는 뜻이므로 실패를 리턴합니다.
// ...
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
그 다음에는 while loop 에 진입합니다.
Redisson 은 스핀락 대신 Pub/Sub 구조를 사용한다고 말씀드렸는데요. 내부적으로는 혹시 스핀락을 사용하는것일까요?
조금 더 살펴보겠습니다.
while loop 진입 후 tryAcquire 메서드를 호출해 락 획득을 시도하네요.
처음과 동일하게 TTL 존재여부에 따라 성공/실패를 리턴합니다.
이후 time 변수를 재계산하여 waitTime 이 경과했는지도 확인하네요.
// ...
while (true) {
// ...
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(long timeout, TimeUnit unit);
해당 코드는 세마포어의 tryAcquire 메서드를 호출하는데요.
즉, 해당 코드에서는 timeout 시간만큼 해당 세마포어의 허가를 얻기위해 대기합니다.
이때, TTL 이 존재하고 time(남은 대기시간) 이 TTL 보다 크면 timeout parameter 로 TTL 을 전달합니다.
TTL 만큼 세마포어 획득을 기다리겠다는 뜻입니다.
그렇지 않다면 남은 대기시간만큼 세마포어 획득을 기다립니다.
왜냐하면 TTL 이 남은 대기시간보다 큰데 TTL 만큼 기다린다면 대기시간이 의미가 없어져서 그렇습니다.
그리고 대기중인 timeout 시간보다 Lock 이 먼저 해제된다면 바로 세마포어의 허가를 받아옵니다.
이후 세마포어의 허가를 얻고 난 이후 다시 time 을 재계산하여 대기시간이 경과했는지 확인합니다.
그리고 while loop 의 처음으로 돌아가 다시 락 획득을 시도해 TTL 결과를 얻어오고 반복합니다.
만약 이때 기존의 락이 해제되지 않았다면 다시 이 과정을 반복하게 됩니다.
그리고 마지막에 채널에 대한 구독을 해제하고 종료합니다.
이 while loop 때문에 Redisson 도 스핀락으로 동작한다고 볼 수도 있지만
이 사이에 pub/sub 과 세마포어 개념이 존재하여 세마포어 허가를 얻은 스레드에 대해서만 접근을 허용해
접근 횟수를 획기적으로 줄였기에 다르게도(?) 볼 수 있지 않을까 생각했습니다.
tryLock 의문...
로컬에서 N 개의 스레드를 생성해 Lock 획득을 시도하며 테스트를 진행하던 중 기존에 알고있던 개념과 다른 부분을 발견했습니다.
N 개의 스레드가 Lock 획득을 기다린다면 저는 당연히 N-1 개의 스레드가 채널을 구독하고 있을것으로 생각했는데요.
실제로는 한개의 스레드만 채널을 구독하고있었습니다.
그래서 구독하는 로직을 다시 살펴보았습니다.
1. entryName 으로 RedissonLockEntry 를 가져옵니다.
2. RedissonLockEntry 와 세마포어에게 락 획득에 관련된 설정을 합니다.
3. CompletableFuture 에 콜백을 등록하고 리턴합니다.
LockEntry 가 존재한다면 바로 리턴하는것을 볼 수 있습니다.
즉, 하나의 Lock 에 대해서는 모든 스레드가 채널을 구독하는것이 아니라 가장 우선순위가 높은 하나의 스레드만 구독을 하는것입니다.
그리고 나머지 스레드는 세마포어의 큐로 들어가 대기를 한다고 볼 수 있습니다.
이후 Redis Pub/Sub 을 통해 락이 해제되었다는 신호를 받으면 세마포어의 큐에서
우선순위가 높은 스레드가 락 획득을 시도하게 됩니다.
만약 Pub/Sub 을 사용하지 않고 세마포어만 두었다면 락 해제에 대한 신호를 받기가 어려워 Pub/Sub 을 이용한것이 아닐까 싶습니다.
그리고 Redis Channel 에 대해 모든 스레드가 구독을 하게되면 그것도 비용이니 Pub/Sub 은 해제를 위한 신호로만 사용하고
실제 대기에 대한 처리는 세마포어를 이용해 자원을 효율적으로 사용한것이 아닐까(?) 추측을 해봅니다.
마지막 부분에 대해서는 문서나 다른 자료들을 찾아봤지만 여의치 않아서 의문형으로 남겨놓았는데요.
혹시라도 잘못된 부분이나 다른 의견이 있으신분들은 자유롭게 코멘트 남겨주시면 감사하겠습니다.
읽어주셔서 감사합니다 :)
reference
- https://incheol-jung.gitbook.io/docs/q-and-a/spring/redisson-trylock
- https://redisson.org/glossary/java-semaphore.html
- https://www.javadoc.io/doc/org.redisson/redisson/2.8.2/org/redisson/api/RLock.html
'동시성 (Concurrency)' 카테고리의 다른 글
[Spring] Redisson 라이브러리를 이용한 Distribute Lock 동시성 처리 (2/2) (0) | 2022.10.19 |
---|---|
[Spring] Redisson 라이브러리를 이용한 Distribute Lock 동시성 처리 (1/2) (7) | 2022.10.04 |
JPA 에서 낙관적 락(Optimistic-Lock)을 이용해 동시성 처리하기 (0) | 2022.08.28 |