일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- API
- mysql
- mirror maker2
- spring kafka
- git
- Entity
- Spring Data JPA
- producer
- centos7
- JPA
- Spring JPA
- ECS
- Kubernetes
- offsetdatetime
- topic생성
- K8s
- spring
- consumer
- entity graph
- CI
- bean
- Kotlin
- transactionaleventlistener
- CodePipeline
- PAGING
- QueryDSL
- Streams
- kafka
- AWS
- cd
- Today
- Total
Yebali
분산락과 Redisson 본문
분산락이란?
여러 프로세스(서버)가 하나의 공유자원을 접근할 때 동시성 이슈와 같은 데이터의 무결성을 해치는 현상을 막기 위한 기술 중 하나로
DB를 활용해 공유자원의 접근을 제한하는 기술이다.
운영체제의 뮤텍스와 유사하며 Redis를 이용한 구현이 가장 흔하다.
Redisson을 사용한 분산락
Redisson은 Jedis, Lettuce같은 Redis 클라이언트의 한 종류로 분산락을 이미 구현된 클라이언트이다.
Redisson을 통해 분산락을 사용한다면 아래와 같은 장점을 가질 수 있다.
1. Lock에 타임아웃이 구현되어 있다.
Redisson의 'tryLock' 메소드에는 타임아웃을 명시하도록 되어있다.
그렇기 때문에 프로세스가 Lock을 획득하기 위해서 무한정 기다리지 않으며, 획득한 Lock을 무한정 가지고 있지 않는다.
- waitTime: Lock을 획득하기 위해 대기할 시간.
- leaseTime: 획득한 Lock이 만료되는 시간.
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {}
2. 스핀 락을 사용하지 않는다.
스핀 락 구조는 Lock을 획득하지 못해 기다리는 프로세스들이 계속해서 Lock을 획득하기 위해 Redis(DB)에 요청하는 방식이다.
이런 구조는 Redis(DB)에 엄청난 부하를 주게 되어 효율적이지 못하다.
Redisson는 이 부분을 pub-sub 구조로 구현되어 Redis(DB)에 부하를 주지 않는다.
Lock을 획득하려는 프로세스들은 해당 Lock에 대한 subscriber가 되고,
Lock이 해제되었다는 메세지를 받으면 그때 다시 Lock을 획득하기를 시도한다.
3. Lua 스크립트를 사용한다.
위와 같은 Lock을 획득하고 과정들은 atomic하게 이루어져야 한다.
Redis는 싱글 스레드 기반의 DB이기 때문에 atomic한 연산을 구현하기 쉽다.
Redisson 내부를 보면 아래처럼 Lua 스크립트를 사용하여 Lock의 획득에 대한 연산을 구현한 것을 볼 수 있다.
자료 구조는 Redis의 'String'을 사용하고 있다.
// Redisson 3.18.0
<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) then " + // KEYS[1]에 대한 값이 존재하지 않으면
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // KEYS[1]의 테이블에서 ARGV[2] 필드 값을 1 증가
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // KEYS[1]의 만료 시간을 ARGV[1]로 설정
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + // KEYS[1]에 대한 값이 존재하면
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // KEYS[1]의 테이블에서 ARGV[2] 필드 값을 1 증가
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // KEYS[1]의 만료 시간을 ARGV[1]로 설정
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);", // KEYS[1]의 만료까지 남을 시간을 반환 (ms)
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
// Redisson 3.22.1
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (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.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
참고
'Backend Common' 카테고리의 다른 글
백엔드 면접 질문 (0) | 2023.08.09 |
---|---|
docker-compose (0) | 2023.07.09 |
[Socket] Socket이란? (0) | 2023.01.17 |
Time zone과 표준시 (0) | 2022.11.27 |
[WebSocket] WebSocket이란? (0) | 2022.09.27 |