들어가기 전에
대규모 서비스 개발에 대한 경험이 없다면, 여러 서버를 운영하는 분산환경에서 락 처리에 관한 고민을 할 필요를 느끼지 못한다. ( 물론 내가 부족한 개발 실력을 가지고 있어서도 그렇지만... )
많은 트래픽을 받는 경우 당연히 수평적 확장이 용이해야 하고, 여러 대의 서버로 API가 분산 호출된다.
서비스 특성 상 트랜잭션이 많이 일어나는 상황이면 동기화된 처리가 필요하고, 여러 서버에 공통된 락을 적용해야 하기 때문에 레디스를 이용하여 분산락을 이용한다.
분산 락은 데이터베이스 등 공통된 저장소를 이용하여 자원이 사용 중인지를 체크한다. 그래서 전체 서버에 동기화된 처리가 가능해진다.
간단한 분산 락 구현하기
- setnx 명령어를 통해 "락이 존재하지 않는다", "존재하지 않는다면 락을 획득한다" 두 연산이 atomic 하게 락을 획득할 수 있도록 한다.
- 계속 락을 획득할 때 까지 시도를 한다. 다만 레디스에 너무 많은 요청이 가지 않도록 약간의 sleep을 걸어주었다
- 연산 수행
- 락 해제
void doProcess() {
String lockKey = "lock";
try {
while (!tryLock(lockKey)) { // (2)
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// (3) do process
} finally {
unlock(lockKey); // (4)
}
}
boolean tryLock(String key) {
return command.setnx(key, "1"); // (1)
}
void unlock(String key) {
command.del(key);
}
위의 분산락 구현의 문제점과 해결 방법
문제점 1 - Lock의 타임아웃이 지정되지 않았음
- 연산(3번 과정)이 실제로 오래 걸려서 다른 스레드에서 thread lock를 기다리게 된다. -> 응답 속도 늦어짐, 레디스 부하 심해짐
- 락을 획든한 서버가 오류때문에 죽어버린다면? 다른 애플리케이션에서 영원히 락을 못 얻게 됨
이러한 문제 때문에 락을 획득하는 최대 허용시간이나 최대 허용 횟수를 정해주는게 좋다
문제점 2 - 레디스에 많은 부하가 감
위의 코드처럼 스핀 락을 사용하면 레디스에 엄청난 부담을 주게 된다.
- sleep 시간 늘리면 되지 않나?? 실제 처리 시간보다 sleep 시간이 길게 되면 엄청난 비효율
해당 문제를 해결해주는 오픈소스 레디스 클라이언트인 Redission 에 대해 알아보자
Redission은 분산 락을 어떻게 구현했을까
Redission은 Jedis, Lettuce 같은 자바 레디스 클라이언트이다.
Netty를 사용하여 non-blocking I/O 를 사용한다. Redission는 레디스가 제공해주는 명령어를 사용하지 않고 Map 같은 자료구조나 Lock 같은 특정 구현체를 이용하여 구현 하였다.
1. Lock에 타임아웃이 구현되어 있음
- 두번째 파라미터 : 혹시나 애플리케이션이 락을 해제해주지 않더라도 leaseTime을 통해 자동으로 락을 해제해준다 -> 문제점 1번 해결
// RedissonLock의 tryLock 메소드 시그니쳐
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
2. 스핀락을 사용하지 않는다
Redission은 스핀란 대신 pubsub 기능을 통해 레디스에 주는 엄청난 트래픽을 줄여주었다. 락이 해제될 때마다 subscribe 하는 클라이언트에게 "나 이제 락 해제했어~" 라고 알려준다.
3. Lua 스크립트를 사용한다.
락에 사용되는 여러 연산은 Atomic 해야 한다. 각 명령어를 따로 보내게 되면 아래와 같은 문제점이 발생한다.
- 락 획득 가능해? OK -----( 그 사이에 다른 곳에서 락 획득 ) ----> 락을 획득하려 햇지만 실패
레디스는 싱글 스레드 기반이기 때문에 그래도 atomic 연산을 비교적 쉽게 구현할 수 있다. 그래서 레디스는 트랜잭션, Lua 스크립트로 atomic 연산을 제공한다
RedissionLock은 Lua 스크립트를 이용하여 레디스에 보내는 요청 수를 현저하게 줄여주어 성능을 훨씬 높여준다.
// in RedissonLock.java
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
해당 글을 참고하며 공부하였습니다
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
'Back-End > Redis' 카테고리의 다른 글
Redis란? (0) | 2020.03.17 |
---|---|
레디스(Redis)의 다양한 활용 사례 (4) | 2020.03.17 |
Redis Spring Boot에 설정하기 및 개요 (0) | 2020.01.24 |