Spring에서 Redis로 분산락 사용하기

김태훈·2024년 1월 22일
0

Spring

목록 보기
16/16

항해99를 진행하면서 팀원분들과 동시성 문제에 대해 이야기를 나누면서 낙관적락, 비관적락에 대해 테스트도 진행해보고 Redis의 분산 락에 대해 알게 되었습니다.

Redis는 Reddison으로 구현하는 분산 락뿐만 아니라, Lettuce의 스핀 락도 존재했는데 스핀 락의 경우 분산 락과 달리, Lock을 얻지 못 하면 계속해서 요청을 보내기 때문에 Redis에 대한 부하가 커지게 되서 Pub / Sub 방식을 사용하는 Redisson으로 하는 것이 좋다고 생각했습니다.
Lettuce로도 구현할 수 있지만 복잡하기 때문에 구현하기 쉬운 Redisson을 사용했습니다.

분산 락, Distributed Lock

하나의 요청이 리소스를 선점하면 나머지의 요청은 대기하고 있습니다.
요청이 끝난 후에 Redis는 대기하고 있던 요청들에게 동시에 메시지를 전달하여 먼저 선점하여 Lock을 획득할 수 있습니다.

Spring에서 예시

...
RLock lock = redissonClient.getLock("test");
lock.lock(10, TimeUnit.SECONDS);

try {
  if (getBoardUserSize(userId) >= 10) {
    throw new IllegalArgumentException("보드의 최대 생성 제한이 넘습니다.");
  } else {
    log.info("보드 생성");
    boardRepository.save(board);
  }
} finally {
  lock.unlock();
}
...

분란락 테스트

저희 팀원들과 아래의 컬리 블로그를 참고하면서 1488번의 board를 짧은 시간 내에 1000번의 좋아요를 누르게 된다면, 1000개가 제대로 반영이 될지 jMeter를 사용해 테스트를 진행했습니다.


처음에 테스트를 진행할 때는 데이터가 제대로 반영되지 않았습니다.
로그를 봤을 때, Redisson Lock Already UnLock이 나오는 것을 볼 수 있습니다.

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {
	private static final String REDISSON_LOCK_PREFIX = "LOCK:";

	private final RedissonClient redissonClient;
	private final AopForTransaction aopForTransaction;

	@Around("@annotation(org.hh99.gradation.aop.DistributedLock)")
	public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

		String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key());
		RLock rLock = redissonClient.getLock(key);

		try {
			boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());  // (2)
			if (!available) {
				return false;
			}

			return aopForTransaction.proceed(joinPoint);
		} catch (InterruptedException e) {
			throw new InterruptedException();
		} finally {
			try {
				rLock.unlock(); 
			} catch (IllegalMonitorStateException e) {
				log.info("Redisson Lock Already UnLock");
			}
		}
	}
}

AOP를 이용하여 분산락을 구현하는 코드입니다. lock을 얻을 때, 대기 시간, 임대 시간, 키를 사용해 얻는 것을 볼 수 있습니다.
그리고 락을 얻고 난 후 타겟 메소드를 실행 후, 락을 해제하는 것을 볼 수 있는데 락이 이미 임대 시간이 지나 해제 된 경우 에러 처리를 합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

	// 락의 이름
	String key();

	// 락의 시간 단위
	TimeUnit timeUnit() default TimeUnit.SECONDS;

	// 락을 획득하기 위해 대기하는 시간
	long waitTime() default 5L;

	// 락 임대시간, 획득한 후 leaseTime 이 지나면 락을 해제
	long leaseTime() default 3L;
}

락의 대기 시간, 임대 시간을 조정하는 코드입니다.
여기서 한 번에 1000개에 대한 스레드를 만들어 입력하다보니 부하가 생겨 1000개의 스레드가 실행되는 시간이 16초 가량 걸렸습니다.


이로 인해 락을 획득 했지만 임대 시간 내에 처리를 못 해 락이 해제되고 해제 되었을 때, 다른 스레드가 자원을 선점하여 처리해 데이터의 무결성을 지키지 못 했습니다.

문제를 해결하기 위해, 임대 시간과 대기 시간을 늘려 해결 한 후 테스트한 모습입니다.








reference

https://helloworld.kurly.com/blog/distributed-redisson-lock/

0개의 댓글