오늘은 레디스는 무엇이고 Redisson에서 분산락을 어떻게 구현하는가를 다뤄보려고 한다.
레디스는 몇가지 특징이 있는데
레디스에 대한 기본적인 설명과 사용법은 이곳, 저곳 에서 쉽게 찾아볼 수 있다.
cache, pipelining, key space 등 여러가지 사용 방법이 있다.
블로그나 공식문서에서 상세하게 다루고 있기 때문에 스킵
레디스의 기본 자료구조는 key-value지만 여러가지 자료구조를 제공한다.
분산락이란 서로 다른 프로세스에서 동일한 공유자원에 접근할 때, 데이터의 원자성을 보장하기위해 활용되는 방법이다.
레디스 공식 페이지에선 Redlock이라는 알고리즘을 제안한다. 보러가기
스프링에서 레디스를 사용하기 위한 클라이언트는 Lettuce, Redisson, Jedis 등등 이 있는데, Redisson이 Redlock 알고리즘을 사용해 분산락을 구현했다.
분산락을 코드와 함께 알아보자
RedissonLock.java 전체 코드
@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.toCompletableFuture().get(time, TimeUnit.MILLISECONDS);
} catch (ExecutionException | TimeoutException e) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
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));
}
lock을 얻는 과정에서 Lua 스크립트와 세마포어를 사용해 동시성을 제어하는데, 이 부분이 궁금하다면 찾아보면 좋을 것 같다.
Lettuce
이전 프로젝트에서 Redis Client는 Lettuce를 사용했었는데, 스핀락이 아닌 분산락을 사용하게 된다면, 더 좋은 퍼포먼스를 낼 수 있다고 생각한다.
Client를 Redisson으로 변경해서 병목을 줄여보자.
참고
https://redis.io/
http://redisgate.kr/redisgate/ent/ent_intro.php