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