Redis 분산락 구현하기

구동현·2024년 4월 28일

현재 프로젝트에서 경매의 입찰 기능을 구현했다.
경매글 1에 대해서 1100원과 1200원이 동시에 입찰이 되었을때,
1200원이 먼저 커밋되고, 1100원이 커밋된다면
최고가가 1100원이 되는 동시성 이슈가 발생한다.

이를 해결하기 위해 락을 걸어줘야한다.


락의 종류는 여러가지가 있다.

  • 낙관적 락
  • 비관적 락
  • 데드 락
  • 분산락

낙관적 락

낙관적 락에서는 충돌이 발생하지 않는다고 낙관적으로 가정을 한다.

비관적 락

비관적 락에서는 충돌이 발생한다고 비관적으로 가정을 한다.

낙관적 락이 비관적 락보다 성능적으로 우수하다.
왜냐하면 낙관적 락에서는 충돌이 일어나지 않을 것이라고 가정해서
락 점유 시간을 최소화를 하기 때문이다.

하지만 충돌이 발생할 경우, 낙관적 락은 개발자가 수동으로 롤백처리를 해줘야 한다.

데드 락

두 트랜잭션이 각각 락을 선점하고 있고 서로의 락에 접근해서 값을 얻어오려고 할 때,
당연히 서로가 락이 걸려있어 둘 중 아무것도 처리가 되지 않는 상태이다.


분산락

분산락은 서버가 여러대인 상황에서 동시성 이슈를 처리하기 위해 사용한다.
서버간의 동기화된 처리가 필요하고, 모든 서버가 공통된 락을 적용해야 하기 때문에,
Redis를 사용한다.

Redisson

레디스 분산락을 구현하기 위해 2가지 방법이 있다.

  • Lettuce
  • Redisson

Lettuce

Spin Lock이라는 기능을 사용해서 구현을 한다.
하지만 Lock의 타임아웃이 설정되지 않아서, 획득하지 못한 경우에는 무한루프를 돌게 된다.

Redisson

Redisson은 Pub/Sub의 방식을 사용해서 분산락을 구현한다.
락을 기다리는 유저가 subscriber가 되어 Lock 해제 이벤트가 Publish될 때까지
대기를 한다.
타임아웃이 지날 경우, 락 획득에 실패했음을 알려줘 무한루프를 돌지 않는다.

타임아웃의 장점에 더불어, Redisson은 Lock interface를 지원하기 때문에 Redisson을 선택하게 되었습니다.


구현과정

1. 의존성 추가

	//redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'

2. config 추가

@Configuration
public class RedissonConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    private static final String REDISSON_HOST_PREFIX = "redis://";

    @Bean
    public RedissonClient redissonClient() {
        RedissonClient redisson = null;
        Config config = new Config();
        config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
        redisson = Redisson.create(config);
        return redisson;
    }
}

3. Annotaion 추가

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

    /**
     * 락의 이름
     */
    String key();

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

    /**
     * 락을 기다리는 시간 (default - 5s)
     * 락 획득을 위해 waitTime 만큼 대기한다
     */
    long waitTime() default 5L;

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

4. AOP 구현

@Slf4j(topic = "RedisLockAspect")
@Aspect
@AllArgsConstructor
@Component
public class RedisLockAspect {

    private final RedissonClient redissonClient;
    private final AopForTransaction aopForTransaction;

    @Around("@annotation(distributedLock)")
    public Object applyLock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock)
        throws Throwable {
        RLock lock = redissonClient.getFairLock(distributedLock.value());

        try {
            boolean isLockSuccess = lock.tryLock(
                distributedLock.waitTime(),
                distributedLock.leaseTime(),
                distributedLock.timeUnit()
            );
            log.info("Lock 획득 성공");

            if (!isLockSuccess) {
                throw new TimeOutLockException("Lock 획득 실패");
            }

            return aopForTransaction.proceed(joinPoint);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new TimeOutLockException("Lock 획득 시 Interrupt 발생");
        } finally {
            log.info("Lock 해제 성공");
            lock.unlock();
        }
    }

}

5. Aop For Transactional

/**
 * AOP에서 트랜잭션 분리를 위한 클래스
 */
@Component
public class AopForTransaction {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

이 과정을 따라 기능을 구현하고 나면, Aop For Transactional에 대한 의문이 생긴다.
jointPoint proceed에 Transactional을 안걸어두면 어떻게 될까?

분산락 상위에 트랜잭션이 걸릴 경우,

그림과 같이 락이 해제된 후에 트랜잭션이 커밋되는 경우가 생긴다.
그럼 여전히 동시성 이슈가 존재한다는 것이다.

그렇기 때문에, Propagation에 Requires new 를 적용한 Transaction을 joint proceed에 걸어둔다.

이를 통해 상위에 트랜잭션과 상관없이 트랜잭션이 커밋된 후에 락이 해제가 된다.


참고자료:

https://velog.io/@soyeon207/DB-Lock-%EC%B4%9D-%EC%A0%95%EB%A6%AC-2-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD-%EB%B6%84%EC%82%B0%EB%9D%BD-%EB%8D%B0%EB%93%9C%EB%9D%BD

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

profile
개발합시다

0개의 댓글