[Java] Redis을 활용한 다양한 분산락 구현 방안들

Hyo Kyun Lee·2025년 4월 30일
0

Java

목록 보기
93/93
post-thumbnail

1. 개요

분산락을 구현하기 전에, 일단 먼저 Redis를 왜 사용하는지부터 고민해야 한다.

  • Redis는 왜 사용하는가? 분산락 구현할 때 Redis가 가장 인기있는 도구이기 때문에?
  • "분산락을 제공하기 때문에 Redis를 선택하였다"라는 도구적 관점에서 접근하면 안된다.

1) DB가 이중화되어 단순 자원의 비관/낙관락을 통한 동시성 제어가 불가능하여 다른 방안을 찾아야 할 경우
2) key-value의 형태로 데이터를 간편하게 저장하고 이후 조회에 대한 I/O 비용을 감소하기 위한 방안을 찾아야 할 경우
3) 캐싱처리에 대한 전략을 세우고 이에 대해 지원하는 프레임워크가 존재할 경우
4) 오픈소스 기반으로 되어있어 확장성이 높고, 유연한 대응을 기대하여야 할 경우

이에 대한 전략적 선택으로 Redis를 고려할 수 있고, 그만큼 활용도나 상용도가 높아 즉시적인 best practice를 기대할 수 있는 관점에서 접근하여야 한다.

Redis와 같은 nosql 데이터베이스는 많다. 하지만 Redis와 비교하였을때 지원수준이나 활용방안 등에 대해서 유리하다고 볼 수 없다. 예를 들어 MongoDB는 주로 문서 데이터베이스로, 분산 락 구현에 최적화된 데이터베이스라고는 볼 수 없다. 또한 Redis에 비해 MongoDB는 라이브러리나 트랜잭션 지원 등이 제한적이고, 응답 시간이나 동시성 처리 능력이 높지 않다.

그리고 Redis는 key-value 형태의 데이터베이스이지만, 자료구조적인 측면에서 매우 다양한 방법을 지원하므로(Strings/Sets 등) 활용도가 높은 것도 채택이유 중 하나일 것이다(*Redis의 다양한 자료구조 참고).

이러한 고민을 거치고 Redis를 분산락 구현을 위한 전략으로 선택할 준비가 되었다면, 어떠한 방식으로 분산락을 구현할 수 있을지 살펴보면 될 것이다.

2. Redis 활용방안에 대한 고민

분산락 구현을 위해 Redis 활용방안에 대해 먼저 고민하였다. 일전의 분산락/낙관락과 달리 Redis는 락의 초점이 자원이 아닌 서비스에 있기 때문이다.

또한 Redis를 통해 서비스를 잠구고 해제한다를 넘어, 나아가 "다양한 서비스 요청"을 Redis에서 제어할 수 있고 이를 통해 DB 분산을 줄여줄 수 있다는 "분산"의 개념을 구현하기 위해 고민하기도 하였다.

1) 락 범위 조정

  • 사용자 요청에 대한 락, 동시성 제어가 필요한 요청이 다수 발생하더라도 모든 요청에 대해 락을 걸 수는 없고 비효율적인 락으로 인해 성능저하를 유발해서는 곤란하다.
  • 사용자에 상관없이 요청 그 자체를 key로 지정을 해야하는 것은 비효율적이라는 판단이 들었기에, 사용자 별 요청을 구분하여 락을 걸 수 있는 userId를 key로 지정하여 락 범위를 조정하였다.

2) 분산락 구현 후 후속처리

  • key를 획득하였다고 하여 분산락을 완전히 구현한 것은 아니다.
  • getLock 이후 tryLock을 왜 굳이 사용하나 싶었는데, key 획득 실패여부에 따라 트랜잭션을 중단하거나 지속하는 등의 후속 진행 방안이 반드시 있어야 한다는 생각이 들었다.
  • 후속 진행 방법도 결국 DB 부하를 분산할 수 있는 방안이 될 수 있으므로, 후속 처리 방안도 나름 고민해볼 필요가 있었다.

3) Redis AOP

  • Redis AOP는 쉽게 말하면 Distributed Lock을 어노테이션으로 설정하여, 동적으로 쉽게 분산락을 구현하겠다라는 방안이다.
  • AOP에 대한 개념이나 구현은 거의 비슷하였고 프로그래밍 패턴도 동일하였는데, 그대로 적용하는 것은 의미가 없다는 판단이 들었고, Redis AOP를 활용한 Redisson 분산락을 구현하기 위해 나름의 방법을 찾아 다양하게 적용해보았다.

3-1. Redis 구현 시나리오 - ASIS

처음에는 Distributed Lock 에 사용하는 lock 구현을 서비스에 "직접" 적용하는 것은 어떨까 생각해보았는데, 아래와 같이 한계점이 분명하였다.

  • 기본적으로 모든 도메인에서 Distributed Lock을 사용할 수 있다는 생각으로, 서비스 내에서 직접 락 로직을 적용하는 것은 무리가 있다.
  • 직접 로직을 작성하여 발생하는 중복으로 인해 클린코드에서 멀어질 수 있다.

3-2. Redis 구현 시나리오 - TOBE

일단 기본적인 Redis 구축 시나리오부터 시작하여 개선방안을 구성하였다.

  • 무작정 Redis를 공통구현요소로 보기보다는, Redis는 서비스 요청마다 그 구현 방안이 달라질 수 있기에, Config 정도만 공통 요소로 구성하고 각 main 도메인 내 Redis 하위 도메인을 별도로 구성하여 이를 활용하도록 한다.
  • Distributed Lock 어노테이션 및 포인트 컷 등 분산락 공통 로직을 구현하며 AOP/포인트컷 등을 통해 분산락이 필요한 서비스가 해당 어노테이션을 사용할 수 있도록 한다.

이에 대해 Redis 구현방안은 두가지가 있을 수 있다고 생각하였고, 이에 대해 구현을 시도하였다.

  • Redis domain에서 분산락 어노테이션을 구현한 후 Facade Service를 통해 해당 어노테이션을 활용하는 방안, Facade Service를 별도로 구성하여 기존 서비스를 주입(호출)받아 사용한다.
  • Redis domain에서 분산락 어노테이션을 구현한 후 Unit Service 중 분산락이 필요한 곳에 해당 어노테이션을 활용하는 방안, Customized 어노테이션/AOP 등의 의미를 최대화할 수 있는 방법이라 생각하였다.

※ 최종적으로는 어노테이션만을 활용해서 분산락을 구현할 수 있는, 동적으로 유연한 대응이 가능하도록 하는 방안으로 분산락을 구현하기 위해 노력하였다.

4. Redis 설치

먼저 Redis github에 들어가서 Redis를 다운로드해야 한다.

반드시 program files/Redis의 경로로 다운로드해야 redis-cli 등을 정상적으로 활용할 수 있다.

아래와 같이 서버를 먼저 실행한 후, redis-cli.exe를 실행하여 서버 동작을 확인해야 한다.

5-1. 분산락 구현 방안 1 - simple lock

simple lock은 redis에 key가 있다면 현재 진행 중인 서비스의 종료를 기다리지 않고 바로 현재 서비스를 중단한다. 이후의 후속 작업은 없다.

lock을 제공하는 Provider 클래스를 구성하되, 해당 요소는 각 도메인 별로 필요한 부분이 달라질 수 있다고 판단하여 최상위 공통요소가 아닌 도메인의 lock 제공자(infra 계층)로 구성하였다.

@Component
@RequiredArgsConstructor
public class LettuceProvider {
		
		/*
		 * redis key값은 userId로 설정
		 * */
		
	 	private final RedisTemplate<String, String> redisTemplate;

	    public Boolean lock(Long key) {
	    	/*
	    	 * Lettuce : Spin Lock
	    	 * redis key 값을 userId로 설정하며, key값이 있다면 false를 반환한다.
	    	 * 3000ms 후 redis의 key는 만료(삭제).
	    	 * */
	        return redisTemplate
	                .opsForValue()
	                .setIfAbsent(this.getKey(key), "lock", Duration.ofMillis(3_000));
	    }

	    public Boolean unlock(Long key) {
	    	/*
	    	 * redis key 값을 제거하여 분산락 상태를 초기화한다.
	    	 * */
	        return redisTemplate.delete(this.getKey(key));
	    }
	    
	    private String getKey(Long key) {
	    	/*
	    	 * userId type인 Long을 String 형태로 변환한다.
	    	 * */
	    	return String.valueOf(key);
	    }
}

그리고 도메인 별 필요한 Facade Service를 구성하고, 위에서 만들어 놓은 Provider와 동시성 제어가 필요한 단위 서비스를 조합하여 하나의 통합 서비스를 구축하였다.

  • Redis라는 도메인
  • 동시성 제어가 필요한 서비스

의 두가지 이상의 도메인, 서비스 조합이 필요하다는 판단으로 Facade Service를 새롭게 구성하였다.

다만 구현을 진행하면서 다소 번거롭다는 생각이 들었는데, 좀 더 객체지향적이고 중복을 줄일 수 있는 방안을 한번 고민해볼 필요가 있겠다.

public void charge(PointDTO pointDTO) {
		/*
		 * redis에서 userId 유무를 확인하여
		 * 있으면 트랜잭션을 중단합니다.
		 * */
		if(lettuceProvider.lock(pointDTO.getUserId())) {
			log.info("lock이 있다면 트랜잭션을 아예 중단합니다.");
			try {
				transactionManager.rollback();
			} catch (IllegalStateException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (SystemException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}else {
			/*
			 * 없을 경우에 트랜잭션을 진행합니다.
			 * */
			try {
				/*
				 * 트랜잭션 전에 분산락 설정
				 * */
				lettuceProvider.lock(pointDTO.getUserId());
				pointWriterService.charge(pointDTO);
			} finally {
				/*
				 * 트랜잭션 진행 후 분산락 해제
				 * */
				lettuceProvider.unlock(pointDTO.getUserId());
			}
		}
	}
 }

5-2. 분산락 구현 방안 2 - spin lock

spin lock은 redis에 key가 있다면 현재 진행 중인 서비스의 종료 및 락 해제를 기다린다, 즉 본인의 서비스가 락을 획득하고 트랜잭션을 마치고 락 해제까지 모든 과정을 마칠때까지 락 획득을 시도한다.

락 획득을 계속 시도하는 만큼 Redis 활용 의미가 없을 정도로 부하가 발생할 것이며, 불필요한 성능 저하로 인해 불리한 점이 많은 락이라 볼 수 있겠다.

위에서 구성한 Provider 및 Facade Service를 그대로 활용하되, 락 획득을 지속적으로 시도하는 부분을 새롭게 구현하였다.

/*
 * 기존 구현하였던 서비스의 Transactional 어노테이션 등
 * 환경설정, 계층구조 등을 바꾸지 않고 Redis 도메인과 Service 도메인을 별도로 두어 Facade화 하여 서비스 조합의 관점으로 구축
 * */
@Service
@Slf4j
public class PointFacadeWriterService {
	
	/*
	 * 프록시를 빈형태로 만들어준 포인트 서비스 프록시를 호출해야 트랜잭션이 호출됨
	 * 즉 Transactional을 적용하기 위해선 pointWriterService를 호출해야 하며
	 * 이를 Lock이 더 우선적(상위계층에서)으로 트랜잭션 서비스를 Wrap하여 모든 트랜잭션의 원자성을 보장한다.
	 * */
	
	@Autowired
	LettuceProvider lettuceProvider;
	
	@Autowired
	PointWriterService pointWriterService;
	
	public void charge(PointDTO pointDTO) {
		/*
		 * redis에서 userId 유무를 확인하여
		 * 있으면 3초동안 계속 요청하고, 없으면 포인트를 충전한다.
		 * */
		while(!lettuceProvider.lock(pointDTO.getUserId())) {
			log.info("spin lock은 주기적으로 redis에 요청합니다. redis 부하를 줄일 수 있는 방안 중 하나입니다.");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		try {
			pointWriterService.charge(pointDTO);
		} finally {
			lettuceProvider.unlock(pointDTO.getUserId());
		}
	}
}

5-3. 분산락 구현 방안 3 - Redisson

분산락의 핵심이자 Redis를 활용하는 이유인 Redisson 방식의 분산락이다. 특정 key값에 대해 sub(구독)을 하고, key 값의 잠금/해제에 따른 발행(pub)을 감지하여 상황에 따른 락 획득과 서비스 시도를 진행한다.

지속적으로 락 요청을 발생시키지도 않고, 특정 key값에 대한 구독과 발행을 기반으로 락획득을 시도하기 때문에 락범위를 적절하게 조정하며 적용 시점까지 알맞게 구현할 수 있는 방안이라 할 수 있겠다.

5-3-1. Redis 도메인 별도 분리 및 Facade Service를 통한 구현

위와 마찬가지로 Redis 도메인을 별도로 분리하여 Redisson 구현에 필요한 부분들을 infra 등에 구현하고, 이를 단위 서비스와 Facade Service에서 통합 구현하는 방안을 생각하였다.

먼저 DistributedLock Customized 인터페이스(어노테이션)는 모든 도메인에 적용할 수 있는 공통요소가 아닌, 각 도메인별 Customizing이 필요할 것으로 판단하여 도메인 하위 interfaces(지금보니 infra 계층에 두는 것이 적당할 것으로 보임) 계층에 구현하였다. 이 어노테이션을 락을 적용하기 위한 메소드에 붙이므로 Target을 method로 지정하였다.

/*
 * Point 도메인에서 사용하는 DistributedLock 인터페이스
 * - Target : method
 * - JVM load 및 RUNTIME 환경에서 사용 : RETENTION TYPE = RUNTIME
 * */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedPointLock {
	
	 /*
     * 락의 key값
     * = userId
     */
    String key();
    
    /*
     * 락의 시간 단위
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    
    /*
     * 락을 기다리는 시간 (default - 3s)
     * 락 획득을 위해 waitTime 만큼 대기한다
     */
    long waitTime() default 3L;

    /*
     * 락 해제를 위한 임계 시간 (default - 3s)
     * 락을 획득한 이후 leaseTime 이 지나면 락을 해제한다
     */
    long leaseTime() default 3L;
    
}

그리고 가장 중요한 포인트컷(Around)를 활용하여 분산락이 트랜잭션 전후로 잠금 및 해제를 할 수 있도록 프록시 객체를 구성하였다. 이 역시 위와 동일한 이유로 하위 infra 계층에 구성해주었다.

위에서 지정한 어노테이션을 메소드에 붙이면, 해당 메소드를 실행할때 프록시 객체가 실행되고 포인트컷(Around)로 인해 해당 어노테이션을 실행하는 전/후 시점에 아래에 프록시 클래스로 정의한 횡단 관심사를 실행하게 된다.

이로 인해 "트랜잭션 전후 락 잠금 및 해제"가 가능해지기에 트랜잭션의 원자성을 보장할 수 있게 되어 분산락 적용이 가능해지는 것이다.

Order(1)을 활용하여 트랜잭션이 발생하기 전에 해당 횡단 관심사(락)를 실행하도록 구성해주었다.

/*
 * Distributed lock 어노테이션을 적용하였을때
 * AOP 횡단관심사를 설정하여 해당 어노테이션 전/후로 횡단 관심사를 실행하도록 하는 프록시객체 => @Aspect / @Component
 * 이 Proxy 객체에 설정된 횡단 관심사는 가장 먼저 실행되도록 설정 => @Order
 * 이는 별도로 설정한 Redis 도메인에서 해당 기능 제공하는 클래스로 간주
 * */

@Order(1)
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedPointLockProxyClass {
	
	/*
	 * 생성자 기반 DI
	 * - Autowired를 사용하지 않고도 생성자 기반의 의존성을 주입받을 수 있다.
	 * */
	private final RedissonClient redissonClient;
	private final AopForTransaction aopForTransaction;
	
	/*
	 * Distributed Lock 어노테이션을 사용한 메소드는
	 * AOP를 통해 Proxy 객체가 작동하여 메소드 실행 전 후(Around) 해당 관심사를 실행하게 됨
	 * */
	@Around("@annotation(kr.hhplus.be.server.point.interfaces.DistributedPointLock)")
    public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        
        DistributedPointLock distributedPointLock = method.getAnnotation(DistributedPointLock.class);
        //distributedPointLock.
        /*
         * 서비스에서 Distributed Lock 어노테이션을 사용하여 lock name을 지정하는 방법 사용
         * */
        
        /*
         * SPEL을 사용하지 않을 경우 메소드에서 전달받은 매개변수로 key값을 설정할 수 있습니다.
         * 하지만 매개변수가 달라질 경우 동적으로 key값을 구성하기가 복잡해지게 됩니다.
         * */
        //Object[] args = joinPoint.getArgs();
        //String key = args[1].toString();
        
        /*
         * SPEL을 사용할 경우 동적인 key값 매핑 및 구성이 가능해집니다.
         * - SPEL을 사용하여 distributed key 값을 동적으로 파싱하여 구성
         * */
        String key = String.valueOf(SpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedPointLock.key()));
        
        /*
         * 지정한 key값에 대한 Redisson 구현체를 구성해줍니다.
         * */
        RLock rLock = redissonClient.getLock(key); 
        
        /*
         * 해당 key 값에 대한 Redisson 구현체를 바탕으로 pub/sub을 진행하여 락 획득을 시도합니다.
         * - pub : 해당 key값에 대한 이벤트를 리스닝
         * - sub : 해당 key값에 대한 이벤트 발생 시 반응하여 동작 수행
         * */
        try {
            boolean available = rLock.tryLock(distributedPointLock.waitTime(), distributedPointLock.leaseTime(), distributedPointLock.timeUnit());  // (2)
            if (!available) {
                return false;
            }
            	
            /*
             * 락 획득 후 트랜잭션 진행합니다.
             * 외부 클래스를 통해 별도 Transational 동작을 수행하는 AOP가 생성되어 메소드의 트랜잭션을 보장합니다.
             * */
            return aopForTransaction.proceed(joinPoint); 
        } catch (InterruptedException e) {
            throw new InterruptedException();
        } finally {
            try {
            	/*
            	 * 트랜잭션 종료 시 성공 실패여부 상관없이 반드시 락을 해제합니다.
            	 * */
                rLock.unlock();
            } catch (IllegalMonitorStateException e) {
                log.info("Redisson Lock Already UnLock {} " , key);
            }
        }
    }
}    

이를 Facade Service를 새롭게 구성하여 적용한 방안이 1차 구현 방안이고, 별도의 unit service를 외부에서 주입받아 안전하게 분산락을 사용할 수 있다는 장점을 의도하였다.

/*
	 * 방안 1 : redis 도메인에서 포인트 충전 서비스를 호출한다.
	 * - 가장 안전한 방법일 수 있지만 어노테이션을 우회하여 사용하는 느낌이고
	 * - distribution lock 어노테이션을 사실상 만들 이유가 없어지므로 굳이라는 생각이 듦
	 * */
	@DistributedPointLock(key = "#pointDTO.getUserId()")
	public void charge(PointDTO pointDTO) {
		pointWriterService.charge(pointDTO);
	}

하지만 생각해보니, 힘들게 구성한 Distributed Lock 어노테이션을 바로 사용하지 않고 Facade Service를 만들어 굳이 서비스를 호출하면서까지 사용하는 것은 비용 낭비라는 생각이 들었다.

특히나 AOP를 활용하여 락 잠금 - 트랜잭션 - 락 해제까지의 과정이 이루어질 수 있도록 구성하였기에, 좀 더 확장적이고 유연한 어노테이션 활용을 위해 직접적으로 서비스에 활용하는 방안을 생각하였다.

5-3-2. 분산락 어노테이션을 서비스에 직접 활용

그래서 최종적으로 아래와 같이 분산락 어노테이션을 직접 단위 서비스에 활용하는 방안을 구현하였다.

다만 위 방법도 가능한 방법이므로, 프로젝트의 상황이나 구성 방안에 따라 적절하게 혼용하면서 분산락을 구현하면 좋을 것 같다.

/*
	 * 분산락 AOP를 활용한 구현
	 * 이때 내부 Around Advice 및 order로 인해 가장 먼저 분산락 AOP가 실행되며 그 후 트랜잭션 진행합니다.
	 * */
	@DistributedPointLock(key = "#pointDTO.getUserId()")
	@Transactional
	public void charge(PointDTO pointDTO) {
		pointWriterRepository.charge(pointDTO);
	}

5-3-3. 테스트 결과

테스트 결과 두 Redisson 분산락 방안 모두 동시성 제어를 성공하였다.

또한 각 5개의 스레드로 동시성 상황이 발생하였을때, 하나의 스레드가 락을 선점하면 다른 스레드들은 락 선점에 실패하는 모습을 볼 수 있었다.

output
2025-05-01 00:05:48.825  INFO 3952 --- [ool-4-thread-1] get lock success 1
2025-05-01 00:05:48.826  INFO 3952 --- [pool-4-thread-5] get lock failure 1
2025-05-01 00:05:48.827  INFO 3952 --- [ool-4-thread-4] get lock failure 1
2025-05-01 00:05:48.829  INFO 3952 --- [pool-4-thread-3] get lock failure 1
2025-05-01 00:05:48.831  INFO 3952 --- [pool-4-thread-2] get lock failure 1

6. getLock - tryLock

분산락 구현 부분 중 흥미로운 부분은 getLock과 tryLock이었다.

처음 보았을때는 단순히 락을 획득하였는데 또 락 획득을 시도한다? 불필요한 중복이 아닌가 싶었는데, Redisson 구현체를 얻고 이에 대해 pub/sub 동작을 수행하는 부분이 나뉘어져 동작하고 있었다.

주석에서 tryLock에 대해 아래와 같이 적어두었다.

/*
         * 지정한 key값에 대한 Redisson 구현체를 구성해줍니다.
         * */
        RLock rLock = redissonClient.getLock(key); 
        
        /*
         * 해당 key 값에 대한 Redisson 구현체를 바탕으로 pub/sub을 진행하여 락 획득을 시도합니다.
         * - pub : 해당 key값에 대한 이벤트를 리스닝
         * - sub : 해당 key값에 대한 이벤트 발생 시 반응하여 동작 수행
         * */
        try {
            boolean available = rLock.tryLock(distributedPointLock.waitTime(), distributedPointLock.leaseTime(), distributedPointLock.timeUnit());  // (2)
           
           ....
*/           

즉, tryLock은 getLock으로 일단 key에 대한 구현체를 만든 후 본격적인 sub/pub을 진행하는 시점으로, tryLock을 통해 락 획득 실패 시 트랜잭션은 중단, 성공하였다면 트랜잭션을 진행시킨다.

참고로 getLock, tryLock을 파고들어가 보면 아래와 같이 볼 수 있다.

6-1. getLock : Redisson 구현체를 만든다.

RedissonClient.getLock(key)를 통해 락범위를 설정하고 후속 작업을 진행할 수 있는 Redisson 구현체를 만든다.

public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.id = getServiceManager().getId();
        this.internalLockLeaseTime = getServiceManager().getCfg().getLockWatchdogTimeout();
        this.entryName = id + ":" + name;
    }

6-2. tryLock : Redisson 구현체에 대한 구독/발행을 지정 시간 동안 동작하고 그 이후에는 구독을 취소한다.

구현체에 대한 구독/발행을 tryLock에서 지정한 시간 동안 동작하고, 그 이후에는 구독 발행을 중단한다.

이 tryLock을 통해 락을 획득하고 최종적으로 unlock을 통해 락을 해제한다.

@Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }
        
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        
        current = System.currentTimeMillis();
        
        /*
        * 이 부분 주목 : 지정한 key 값이 Entry에 넣어짐
        * 그 이후 CompletableFuture에 의해 해당 key값 반환을 감지
        * 락 획득 성공할 경우 최종적으로 true 반환
        * 락 획득 실패할 경우 false 반환
        * 해당 tryLock 시도 동안만 감지하고 이 이후에는 구독 중단
        */
        CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);    
        try {
            subscribeFuture.get(time, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
                    "Unable to acquire subscription lock after " + time + "ms. " +
                            "Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
                subscribeFuture.whenComplete((res, ex) -> {
                    if (ex == null) {
                        unsubscribe(res, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        } catch (ExecutionException e) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        try {
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        
            while (true) {
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }

7. 참고자료

  • Redis 자료구조

Redis 자료구조 - https://han-py.tistory.com/390

  • AOP

Redis AOP 자료 #1 - https://velog.io/@meong/SpringBoot-Redisson-AOP-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EC%9E%AC%EA%B3%A0-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4
Redis AOP 자료 #2 - (https://velog.io/@daehoon12/%EC%A3%BC%EB%AC%B8-%EC%8B%9C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0
Redis AOP 자료 #3 - https://helloworld.kurly.com/blog/distributed-redisson-lock/
Redis AOP 자료 #4 - https://jongmin4943.tistory.com/entry/Spring-Redisson-%EB%B6%84%EC%82%B0%EB%9D%BDDistribute-Lock-%EC%A2%80-%EB%8D%94-%EC%9E%98-%EC%8D%A8%EB%B3%B4%EA%B8%B0-23-AOP

  • Advice

Redis AOP Around Advice 원리 - https://developer-joe.tistory.com/221
AOP/pointcut/JointPoint - https://velog.io/@solar/AOP-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%A1%9C%EA%B7%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%82%A8%EA%B8%B0%EA%B8%B0

  • Customized 어노테이션

어노테이션 구성하기 - https://velog.io/@iniestar/Java-interface-class
서비스 단위의 key값 전달](https://velog.io/@meong/SpringBoot-Redisson-AOP-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EC%9E%AC%EA%B3%A0-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4
매개변수 및 jointpoint의 args를 파싱하여 key값 전달 - https://velog.io/@solar/AOP-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%A1%9C%EA%B7%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%82%A8%EA%B8%B0%EA%B8%B0
SPEL을 통한 key값 전달 - https://velog.io/@daehoon12/%EC%A3%BC%EB%AC%B8-%EC%8B%9C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0

  • 트랜잭션 분리(joint - Process)

args - https://www.google.com/search?q=joinPoint.getArgs%28%29%3B&sca_esv=3645c0c05a1f51fb&sxsrf=AHTn8zpN0RNrmqaDeAMZ0PExb6t1MF8ABw%3A1746020102202&source=hp&ei=BicSaOvSCcPP1e8PkLCCkAk&iflsig=ACkRmUkAAAAAaBI1FhjCxL6HT4b1a6tctIgG-fX67NlI&ved=0ahUKEwjr7dbf7_-MAxXDZ_UHHRCYAJIQ4dUDCBk&uact=5&oq=joinPoint.getArgs%28%29%3B&gs_lp=Egdnd3Mtd2l6IhRqb2luUG9pbnQuZ2V0QXJncygpO0jpA1AAWABwAHgAkAEAmAEAoAEAqgEAuAEDyAEA-AEC-AEBmAIAoAIAmAMAkgcAoAcAsgcAuAcA&sclient=gws-wiz
process - https://developer-joe.tistory.com/221

0개의 댓글