여러 서버 노드가 있을 때, 재고 감소같은 로직은 서버 노드 한대만 수행되도록 제한을 두고 싶습니다. 이럴 때 사용하는 것이 분산락입니다.
분산락을 처리하는 방법은 여러가지 있겠지만 레디스로 처리하는 방법을 알아보겠습니다.
스프링에서 사용되는 Redis 라이브러리는 여러가지가 있지만, Redisson 을 이용해서 구현을 해봤습니다. Redisson은 Redis를 분산 데이터 저장소로 사용할 때 유용합니다. Redisson은 Redis의 Pub/sub 기능을 사용해서 Lock 획득을 재시도합니다. 특정 채널을 구독하고 이벤트를 받기 때문에 이와 같은 처리가 가능합니다.
[설정]
build.gradle에 다음과 같이 라이브러리를 추가합니다.
// file: build.gradle
// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.18.0'
application.yml에 다음과 같이 추가합니다.
spring:
redis:
host: #{redis_호스트_URI}
port: #{port, 기본값: 6379}
db: #{원하는 값}
RedisConfig.java 파일을 다음과 같이 추가합니다.
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Value("${spring.redis.host:localhost}")
private String redisHost;
@Value("${spring.redis.port:6379}")
private String redisPort;
@Value("${spring.redis.db:3}")
private int redisDb;
@Bean
public RedissonClient redissonClient() {
final Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setDatabase(redisDb);
return Redisson.create(config);
}
}
[AOP 처리]
이제 코드를 작성할 차례입니다. 분산락 처리를 어떻게 할지 고민을 했는데 어노테이션을 이용한 AOP 처리가 깔끔해 보였습니다. 어노테이션 코드부터 먼저 보여 드리겠습니다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {
String key();
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
// 락 획득 대기시간
long waitTime() default 500L;
// 락 점유 시간: leaseTime이 지나면 락 해제
long leaseTime() default 60 * 60 * 1_000L;
}
각 속성은 이러한 의미를 가집니다.
- key: 레디스의 데이터가 저장될 때 사용될 키 값
- timeUnit: 레디스 락을 획득할 때 사용되는 시간 단위입니다. ns, ms 부터 hr, days 까지 지정할 수 있습니다.
- waitTime: 락을 획득까지 대기하는 시간을 의미합니다.
- leaseTime: 락 점유 시간을 의미합니다.
락을 획득까지 대기하는 시간은 최대한 짧게 하는 것이 좋다고 생각이 됩니다. 이는 요청에 의해 처리되는 스레드가 너무 오랫동안 대기를 하게 되면 다른 요청 처리를 못하고 시스템 다운타임이 길어지는 원인이 될 수 있습니다. 락 임대 시간을 너무 짧게 하면 프로세스가 끝나기도 전에 락이 풀려서 다른 요청이 처리될 수 있으므로 길게 잡는 것이 좋다고 생각됩니다.
어노테이션은 다음과 같이 지정해서 사용할 수 있습니다.
@DistributedLock(
key = "#{락 이름}",
waitTime = #{대기 시간, 선택사항},
leaseTime = #{점유 시간, 선택사항}
)
public void method(...) {...}
어노테이션을 이용해서 락을 획득하고 해제하는 코드는 다음과 같습니다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {
private static final String REDISSON_LOCK_PREFIX = "LOCK:";
private final RedissonClient redissonClient;
@Around("@annotation({{#경로}.DistributedLock)")
public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
final MethodSignature signature = (MethodSignature) joinPoint.getSignature();
final Method method = signature.getMethod();
final DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
final String lockKey = REDISSON_LOCK_PREFIX + distributedLock.key();
final RLock rLock = redissonClient.getLock(lockKey);
try{
final boolean available = rLock.tryLock(distributedLock.waitTime(),
distributedLock.leaseTime(), distributedLock.timeUnit());
if(!available){
throw new RuntimeException("[Redisson Lock] 락을 획득하는데 실패했습니다.");
}else{
return joinPoint.proceed();
}
} finally {
try {
rLock.unlock();
} catch (Exception e) {
log.error("[Redisson Lock] 락을 해제하는데 실패했습니다. - methodName : {}, key : {}, msg : {}",
method.getName(),
lockKey,
e.getMessage());
}
}
}
}
메서드에 적용된 DistributedLock 객체를 가져옵니다. lockKey를 만들고 redissonClient를 이용하여 락을 가져옵니다. DistributedLock에 지정된 대기 시간과 점유 시간, 시간 단위를 이용하여 락을 점유하려고 시도합니다. 성공하면 메서드를 수행하고, 그렇지 않으면 예외를 던집니다. finally에서는 점유한 락을 해제합니다.
[테스트]
groovy를 이용해서 다음과 같이 테스트 코드를 작성했습니다.
def "분산 락 테스트"(){
given:
...
final Runnable runnable = () -> {
// 생략
}
final Integer concurrentCount = 10
final ExecutorService executorService = Executors.newFixedThreadPool(concurrentCount)
expect:
for(int i=0; i<concurrentCount; i++){
executorService.submit(runnable)
}
Thread.sleep(5 * 60 * 1_000)
}
스레드 풀에 10개의 스레드를 생성하고 동시에 요청을 처리합니다. 맨 처음 락을 점유한 스레드는 한 개를 제외한 나머지는 락 획득에 실패해야 합니다. 실행을 해보면 다음과 같이 콘솔 로그를 확인할 수 있습니다.
https://helloworld.kurly.com/blog/distributed-redisson-lock/
'[개발] 데이터베이스 > Redis' 카테고리의 다른 글
Redis 소개 (0) | 2024.09.02 |
---|