레디스의 분산락은 무엇인가!?

고승원·2023년 2월 15일
2

TIL

목록 보기
8/24

서론

오늘은 레디스는 무엇이고 Redisson에서 분산락을 어떻게 구현하는가를 다뤄보려고 한다.

특징

레디스는 몇가지 특징이 있는데

  • key-value 형식의 데이터 저장소이다.
  • 단일 스레드 실행을 하기 때문에 Atomic 하다.
  • 디스크가 아닌 인메모리에 저장한다.

레디스에 대한 기본적인 설명과 사용법은 이곳, 저곳 에서 쉽게 찾아볼 수 있다.

레디스의 여러가지 사용처

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;
	}
  1. ttl을 요청하고 얻는다면, true 반환 (성공)

	time -= System.currentTimeMillis() - current;
	if (time <= 0) {
		acquireFailed(waitTime, unit, threadId);
		return false;
	}
  1. wait time이 지나면 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;
	}
  1. RedissonLockEntity를 time만큼 구독한다.
	try {
		time -= System.currentTimeMillis() - current;
		if (time <= 0) {
			acquireFailed(waitTime, unit, threadId);
			return false;
		}
  1. lock 요청을 할 수 있을때 다시한번 time이 지나면 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;
			}
  1. ttl 또는 time으로 lock 획득.
			// 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));
}
  1. 구독 취소.

lock을 얻는 과정에서 Lua 스크립트와 세마포어를 사용해 동시성을 제어하는데, 이 부분이 궁금하다면 찾아보면 좋을 것 같다.

Lettuce

  • Lettuce는 스프링부트 2.0부터 레디스 기본 클라이언트로 사용되고 있다.
  • Lettuce는 스핀락(lock을 획득할때 까지 요청) 방식이어서 레디스에게 부하를 준다.
  • 자세한 내용은 Lettuce 공식문서를 참고하자.

todo

이전 프로젝트에서 Redis Client는 Lettuce를 사용했었는데, 스핀락이 아닌 분산락을 사용하게 된다면, 더 좋은 퍼포먼스를 낼 수 있다고 생각한다.
Client를 Redisson으로 변경해서 병목을 줄여보자.


참고
https://redis.io/
http://redisgate.kr/redisgate/ent/ent_intro.php

profile
봄은 영어로 스프링

0개의 댓글