카테고리 없음

쿠폰 선착순 발급 이벤트 - 동시성 이슈 해결 redis lock, 분산락

whyHbr 2024. 11. 30. 16:42
728x90
반응형

지난 시간

https://wonder-why.tistory.com/207

synchronized 를 통해 동시성 문제를 해결, 쿠폰 발급 수량이 정확히 맞춰졌지만, 

이 synchonized 는 자바 애플리케이션에 종속되기 때문에, 여러 서버로 확장이 된다면 락을 제대로 관리 할 수 없어진다.  

이를 해결하기 위해 분산락 구현이 필요하다.  


1. 설정

 

build.gradle에 추가한다.

implementation("org.redisson:redisson-spring-boot-starter:3.16.4")

 

RedisConfiguration 을 정의한다.

package com.example.couponcore.configuration;


import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//redis client 정의
public class RedisConfiguration {

    //yml 파일에서 정의한 값
    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private String port;

    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        String address = "redis://" + host + ":" + port;
        config.useSingleServer().setAddress(address);
        return Redisson.create(config);
    }
}

 

어플리케이션 실행은 잘 되지만, coupon - Tasks - build - build 실행시

CouponApiApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.BeanCreationException at AutowiredAnnotationBeanPostProcessor.java:515
            Caused by: org.springframework.util.PlaceholderResolutionException at PlaceholderResolutionException.java:81
        Caused by: org.springframework.beans.factory.BeanCreationException at AutowiredAnnotationBeanPostProcessor.java:515

            Caused by: org.springframework.util.PlaceholderResolutionException at PlaceholderResolutionException.java:81


1 test completed, 1 failed
Caused by: org.springframework.beans.factory.BeanCreationException at AutowiredAnnotationBeanPostProcessor.java:515

 빈 생성에 실패해 contextLoads() FAILED,   contextloads 가 깨진 것을 볼 수 있다. 


해결 방법:

CouponConsumerApplicatonTests, CouponApiApplicationTests( 멀티모듈의 통합테스트) 에

@ActiveProfiles("test")
@TestPropertySource(properties = "spring.config.name=application-core")

추가하면 빌드가 성공한 걸 볼 수 있다.


 

분산락:

package com.example.couponcore.component;

import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class DistributeLockExecutor {

    private final RedissonClient redissonClient;

    public void execute(String lockName, long waitMilSecond,
            long leaseMillSecond, Runnable runnable) {
        // 락에 대한 객체를 가져온다. getLock 으로 락 이름 지정.
        RLock lock = redissonClient.getLock(lockName);

        try {
            /**
             * 락 획득 시도
             * waitMillSecond: 락 획득 시도하는 것을 얼마나 기다릴 것인지
             * leaseMillSecond: 획득 후, 락 유지 시간
             * TimeUnit: 단위 지정
             */
            boolean isLocked = lock.tryLock(waitMilSecond, leaseMillSecond, TimeUnit.MILLISECONDS);

            //락 획득 실패시
            if (!isLocked) {
                throw new IllegalStateException("[" + lockName + "] 락 획득 실패");
            }
            //획득 성공 하면 로직 실행
            runnable.run();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }finally {
            //락 반환
            if(lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

 

 

적용:

public class CouponIssueRequestService {

    private final CouponIssueService couponIssueService;
    private final DistributeLockExecutor distributeLockExecutor;

    public void issueRequestV1(CouponIssueRequestDto requestDto) {
        distributeLockExecutor.execute("lock_" + requestDto.couponId(), 10000, 10000, () -> {
            /**
             * executor 에 runnable 을 넘겨 execute 안에서 실행 할 수 있도록.
             */
            couponIssueService.issue(requestDto.couponId(), requestDto.userId());

        });
        log.info("쿠폰 발급 완료. couponId: {}, userId: {}", requestDto.couponId(), requestDto.userId());
    }
}

 


 

 

테스트 결과: 

synchronized 를 사용했을 때보다 RPS 가 하락한 것을 볼 수 있다.

 

 

동시성 제어가 된 것을 볼 수 있다. 


레디스 분산락을 사용하면 여러 서버가 띄워져 있을 때에도 동시성 문제를 해결 할 수 있다. 

728x90