개요
분산락은(Distrubuted Lock) 서로 다른 프로레스가 공유 리소스를 상화 배타적인 방식으로 작동해야 하는 분산 환경에서 매우 유용한 방식이다.
Redis를 사용하면 쉽게 분산락을(Distrubuted Lock) 구현할 수 있다. Redis의 데이터 처리는 Single Thread로 처리되며 모든 요청이 순차적으로 처리되기 때문에 DB를 사용한 분산락 보다 쉽게 구현이 가능한다.
Redis Client
Lettuce
Spring에 spring-boot-starter-data-redis를 사용하면 lettuce-core 라이브러리가 기본적으로 redis client로 사용된다. lettuce는 redis lock 관련 기능은 별도로 제공하지 않고, 일반적으로 Spin lock 형태로 별도로 구현한다.
Spin lock은 아래와 같은 형태로 구현하고 lock을 획득할때까지 지속적으로 서버에 요청을 보내며 대기함으로 분산락을 많이 사용한다면 서버에 많은 부하를 줄 수 있다.
while(!lock.acquireLock()) {
Thread.sleep(100L);
}
// DO ......
lock.unlock()
Redisson
Redisson 클라이언트는 Spring redis에서 기본적으로 제공하지는 않지만, Spring Boot integration을 위한 별도 라이브러리를 제공하고 있어서 쉽게 사용가능하다. 또한 Redis Disributed Locks 문서에서(https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) Java 구현체로 소개하고 있다.
Integration with Spring Boot
redisson 라이브러리의 버전은 Spring Boot 버전에 맞게 사용한다. (참고 :https://redisson.org/docs/integration-with-spring/
(사용하지 않는 lettuce 라이브러리는 제거한다.)
dependencies {
implementation 'org.redisson:redisson-spring-boot-starter:3.38.1'
implementation ('org.springframework.boot:spring-boot-starter-data-redis') {
exclude group: 'io.lettuce', module: 'lettuce-core'
}
......
}
분산락 동작 방식
Redisson을 사용한 분산락 사용 방법은 lock으로 사용할 key를 설정하고. 해당 key가 있으면 다른 프로세스는 대기하고. key가 없으면 요청한 프로세스가 lock을 획득한다. 대기하는 프로세스들은 위에 설명한 Spin Lock형태가 아닌 Redis에 pubsub 기능을 사용하여 lock 해제 여부를 대기한다.
Lock 설정
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteSyncedNoRetryAsync(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));
}
Lock 대기
protected CompletableFuture<RedissonLockEntry> subscribe(long threadId) {
return pubSub.subscribe(getEntryName(), getChannelName());
}
protected void unsubscribe(RedissonLockEntry entry, long threadId) {
pubSub.unsubscribe(entry, getEntryName(), getChannelName());
}
Example
10,000개의 스레드를 사용하여 Int 변수를 1씩 증가 시키는 서비스를 구현하여 Lock을 사용한 경우와 사용하지 않는 경우를 비교하여 분산락을 사용했을 때 어떻게 동작하는지 알아본다.
분산락을 사용하지 않는 경우
Virtual Thread 10,000개를 생성하여 CountService의 increase()를 10,000번 호출한다.
Code
@SpringBootApplication
public class DistrubutedLockApplication {
public static void main(String[] args) {
SpringApplication.run(DistrubutedLockApplication.class, args);
}
@Bean
CommandLineRunner run(CountService countService) {
return args -> {
var stopWatch = new StopWatch("Increase 10,000");
stopWatch.start("distributed_lock");
var executor = Executors.newVirtualThreadPerTaskExecutor();
IntStream.range(0, 10_000).forEach(it -> executor.execute(() -> countService.increase()));
countService.printCount();
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
};
}
}
@Service
class CountService {
private int count;
public void increase() {
this.count = this.count + 1;
}
public void printCount() {
System.out.println("count : " + this.count);
}
}
실행 결과
10,000개의 스레드를 사용하여 숫자를 증가 시켰지만 실제 count 값은 9913만 증가되었다.
count : 9913
StopWatch 'Increase 10,000': 0.0082752 seconds
----------------------------------------------
Seconds % Task name
----------------------------------------------
0.0082752 100% distributed_lock
분산락을 사용한 경우
Code
RedissonClient는 lock관련 기능을 제공한다.
tryLock()로 wait time(50)과 lease time을(10) 설정하여 lock을 요청한다.
lock을 획득한 경우 비지니스비즈니스 로직을 처리하고, 획득하지 못한 경우는 비즈니스 로직에 맞게 처리한다.
@Service
class CountService {
@Autowired
private RedissonClient redissonClient;
private int count;
public void increase() {
var lock = this.redissonClient.getLock("distributed_lock_key");
try {
var res = lock.tryLock(50L, 10L, TimeUnit.SECONDS);
if (res) {
try {
this.count = this.count + 1;
} finally {
lock.unlock();
}
} else {
// TODO Lock을 획득하지 못하는 경우 처리
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void printCount() {
System.out.println("count : " + this.count);
}
}
실행 결과
분산락을 사용하여 처리한 결과 정상적으로 10,000개의 count 값을 볼 수 있다.
count : 10000
StopWatch 'Increase 10,000': 38.8818207 seconds
----------------------------------------------
Seconds % Task name
----------------------------------------------
38.8818207 100% distributed_lock
실행 중 Reids의 데이터를 보면 아래와 같이 분산락으로 사용하는 key와 pubsub을 사용하는 2개의 키를 확인할 수 있다.
127.0.0.1:6379> keys *
1) "distributed_lock_key"
2) "redisson_unlock_latch:{distributed_lock_key}:d760313c9459362cec1792da9c1f859"
참고
https://redis.io/docs/latest/develop/use/patterns/distributed-locks/
https://redisson.org/docs/data-and-services/locks-and-synchronizers/
'dev > spring' 카테고리의 다른 글
Spring Batch MongoDB 빠르게 수집하기 (MongoCursorItemReader) (4) | 2024.11.08 |
---|---|
Spring AI를 사용한 AI 어시스턴트 구현 RAG - Part 2 (with OpenAI) (1) | 2024.10.03 |
Spring AI를 사용한 AI 어시스턴트 구현 - Part 1 (with OpenAI) (1) | 2024.10.01 |
[Spring] Rest service with Hexagonal architecture (1) | 2024.09.22 |
[Spring] @Async와 Virtual Thread (1) | 2024.09.14 |