Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions queue/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ dependencies {
//eureka-client
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

//openfeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.27.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'

//embedded redis for test
testImplementation 'it.ozimov:embedded-redis:0.7.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = {
"com.codeterian.queue",
"com.codeterian.common"
})
@EnableFeignClients
@EnableScheduling
public class QueueApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.codeterian.queue.application.service;

import com.codeterian.common.infrastructure.util.Passport;
import com.codeterian.queue.infrastructure.redisson.aspect.DistributedLock;
import com.codeterian.queue.presentation.dto.QueueResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -31,6 +32,7 @@ public class QueueService {
/**
* 사용자를 대기큐 or 실행큐에 추가 (새로고침 시 뒤로 밀리도록 처리)
*/
@DistributedLock(key = "#lockName")
public void joinQueue(Passport passport) {
String userId = passport.getUserId().toString();

Expand All @@ -40,10 +42,10 @@ public void joinQueue(Passport passport) {

Long currentRunningQueueSize = redisTemplate.opsForZSet().size(RUNNING_QUEUE);

if (currentRunningQueueSize != null && currentRunningQueueSize >= TRAFFIC_THRESHOLD) {
// 임계치 초과: 대기큐에 사용자 추가
if (currentRunningQueueSize != null && currentRunningQueueSize > TRAFFIC_THRESHOLD-1) {
redisTemplate.opsForZSet().add(WAITING_QUEUE, userId, System.currentTimeMillis());
log.info("사용자가 대기큐에 추가되었습니다.");

} else {
// 실행큐에 사용자 추가
redisTemplate.opsForZSet().add(RUNNING_QUEUE, userId, System.currentTimeMillis());
Expand All @@ -65,8 +67,10 @@ public void joinQueue(Passport passport) {
@Scheduled(fixedDelay = 2000)
public void getNextUserFrom() {
Set<String> nextUsers = redisTemplate.opsForZSet().range(WAITING_QUEUE, 0, 0);
Long currentRunningQueueSize = redisTemplate.opsForZSet().size(RUNNING_QUEUE);

if (nextUsers != null && !nextUsers.isEmpty()) {
if (nextUsers != null && !nextUsers.isEmpty() &&
currentRunningQueueSize != null && currentRunningQueueSize < TRAFFIC_THRESHOLD) {
String nextUser = nextUsers.iterator().next();
redisTemplate.opsForZSet().remove(WAITING_QUEUE, nextUser);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.codeterian.queue.infrastructure.config;

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;
import org.springframework.data.redis.connection.RedisConnectionFactory;
Expand All @@ -10,6 +14,14 @@
@Configuration
public class RedisConfig {

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

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

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

@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
var redisTemplate = new RedisTemplate<String, String>();
Expand All @@ -21,4 +33,14 @@ public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisC
return redisTemplate;
}

@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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.codeterian.queue.infrastructure.redisson.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class AopForTransaction {

/**
부모 트랜잭션에 관계없이 별도의 트랜잭션으로 동작하도록 설정하여 반드시 트랜잭션 커밋 이후
락이 해제되도록 REQUIRES_NEW 옵션 부여
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.codeterian.queue.infrastructure.redisson.aspect;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

/**
* CustomSpringELParser 는 전달받은 락의 이름을 Spring Expression Language 로 파싱하여 읽어옵니다.
*/
public class CustomSpringELParser {

private CustomSpringELParser(){}

public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}

return parser.parseExpression(key).getValue(context, Object.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.codeterian.queue.infrastructure.redisson.aspect;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class DistributeLockAop {

private static final String REDISSON_LOCK_PREFIX = "LOCK: ";

private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;

@Around("@annotation(com.codeterian.queue.infrastructure.redisson.aspect.DistributedLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) {
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 lockable = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
if (!lockable) {
log.info("Lock 획득 실패={}", key);
return false;
}

log.info("로직 수행");
return aopForTransaction.proceed(joinPoint);

} catch (Throwable e) {
log.info("에러 발생!");
throw new RuntimeException(e);
} finally {
rLock.unlock();
log.info("이미 언락 상태 입니다.");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.codeterian.queue.infrastructure.redisson.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

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

//락의 이름
String key();

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

long waitTime() default 5000L; // 락 획득을 시도하는 최대 시간 (ms)
long leaseTime() default 3000L; // 락을 획득한 후, 점유하는 최대 시간 (ms)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -21,7 +20,7 @@ public class QueueController {


//사용자 대기열 진입
@PostMapping("/join")
@GetMapping("/join")
public ResponseEntity<ResponseDto<Void>> joinQueue(@CurrentPassport Passport passport) {

//서비스 로직
Expand Down