Skip to content

Commit 6600de0

Browse files
authored
feat: redis분산락 구현 (#179)
* feaet: redis분산락 구현 * 수정: 퍼블릭주소, yml변경
1 parent 2a5bd10 commit 6600de0

File tree

11 files changed

+296
-36
lines changed

11 files changed

+296
-36
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ dependencies {
3939
runtimeOnly 'com.mysql:mysql-connector-j'
4040
annotationProcessor 'org.projectlombok:lombok'
4141

42+
implementation 'org.redisson:redisson-spring-boot-starter:3.27.0' //Redisson
43+
4244
testImplementation 'com.h2database:h2'
4345
testCompileOnly 'org.projectlombok:lombok' // 테스트 의존성 추가
4446
testAnnotationProcessor 'org.projectlombok:lombok'

docker-compose.yml

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ services:
1111
depends_on:
1212
- mysqldb
1313
- redis
14+
env_file:
15+
- .env
1416
environment:
1517
- SPRING_PROFILES_ACTIVE=docker
1618
networks:
@@ -61,32 +63,32 @@ services:
6163
networks:
6264
- app-tier
6365

64-
prometheus:
65-
user: "root"
66-
image: prom/prometheus
67-
container_name: prometheus_container
68-
volumes:
69-
- /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/config:/etc/prometheus
70-
- /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/volume:/prometheus/data
71-
ports:
72-
- 9090:9090
73-
command:
74-
- '--config.file=/etc/prometheus/prometheus.yml'
75-
restart: always
76-
networks:
77-
- app-tier
78-
79-
grafana:
80-
user: "root"
81-
image: grafana/grafana
82-
container_name: grafana_container
83-
ports:
84-
- 3000:3000
85-
volumes:
86-
- ./grafana/volume:/var/lib/grafana
87-
restart: always
88-
networks:
89-
- app-tier
66+
# prometheus:
67+
# user: "root"
68+
# image: prom/prometheus
69+
# container_name: prometheus_container
70+
# volumes:
71+
# - /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/config:/etc/prometheus
72+
# - /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/volume:/prometheus/data
73+
# ports:
74+
# - 9090:9090
75+
# command:
76+
# - '--config.file=/etc/prometheus/prometheus.yml'
77+
# restart: always
78+
# networks:
79+
# - app-tier
80+
#
81+
# grafana:
82+
# user: "root"
83+
# image: grafana/grafana
84+
# container_name: grafana_container
85+
# ports:
86+
# - 3000:3000
87+
# volumes:
88+
# - ./grafana/volume:/var/lib/grafana
89+
# restart: always
90+
# networks:
91+
# - app-tier
9092

9193
volumes:
9294
mysqldb-data:

src/main/java/com/threestar/trainus/domain/coupon/user/service/CouponService.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.threestar.trainus.domain.coupon.user.repository.UserCouponRepository;
2121
import com.threestar.trainus.domain.user.entity.User;
2222
import com.threestar.trainus.domain.user.service.UserService;
23+
import com.threestar.trainus.global.annotation.RedissonLock;
2324
import com.threestar.trainus.global.exception.domain.ErrorCode;
2425
import com.threestar.trainus.global.exception.handler.BusinessException;
2526

@@ -34,12 +35,14 @@ public class CouponService {
3435
private final UserService userService;
3536

3637
@Transactional
38+
@RedissonLock(value = "#couponId")
3739
public CreateUserCouponResponseDto createUserCoupon(Long userId, Long couponId) {
3840
User user = userService.getUserById(userId);
39-
Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId)
41+
// Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId)
42+
// .orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND));
43+
Coupon coupon = couponRepository.findById(couponId)
4044
.orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND));
41-
42-
//쿠폰 발급 종료시각이 지났으면 예외처리
45+
// 쿠폰 발급 종료시각이 지났으면 예외처리
4346
if (LocalDateTime.now().isAfter(coupon.getCloseAt())) {
4447
throw new BusinessException(ErrorCode.COUPON_EXPIRED);
4548
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.threestar.trainus.global.annotation;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
@Target({ElementType.METHOD, ElementType.TYPE})
10+
@Retention(RetentionPolicy.RUNTIME)
11+
@Documented
12+
public @interface RedissonLock {
13+
14+
String value(); //Lock 이름
15+
16+
long waitTime() default 5000L; //Lock을 획득을 시도하는 최대 시간 ms
17+
18+
long leaseTime() default 2000L; //락을 획득한 후, 점유하는 최대 시간 ms
19+
}

src/main/java/com/threestar/trainus/global/config/CorsConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ public void addCorsMappings(CorsRegistry registry) {
1212
.allowedOrigins("http://localhost:3000",
1313
"http://localhost:8080",
1414
"http://localhost:8031",
15-
"http://43.202.206.47:8080",
16-
"http://43.202.206.47:3000",
17-
"http://43.202.206.47:8031")
15+
"http://15.165.184.145:8080",
16+
"http://15.165.184.145:3000",
17+
"http://15.165.184.145:8031")
1818
.allowCredentials(true) // 쿠키 허용
1919
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
2020
.allowedHeaders("*");

src/main/java/com/threestar/trainus/global/config/RedisConfig.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.threestar.trainus.global.config;
22

33
import org.springframework.beans.factory.annotation.Value;
4-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
54
import org.springframework.context.annotation.Bean;
65
import org.springframework.context.annotation.Configuration;
76
import org.springframework.context.annotation.Primary;
@@ -10,8 +9,6 @@
109
import org.springframework.data.redis.core.RedisTemplate;
1110
import org.springframework.data.redis.serializer.StringRedisSerializer;
1211

13-
14-
1512
@Configuration
1613
public class RedisConfig {
1714

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.threestar.trainus.global.config;
2+
3+
import org.redisson.Redisson;
4+
import org.redisson.api.RedissonClient;
5+
import org.redisson.config.Config;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class RedissonConfig {
11+
12+
private static final String REDISSON_HOST_PREFIX = "redis://";
13+
14+
@Bean
15+
public RedissonClient redissonClient() {
16+
Config config = new Config();
17+
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + "localhost:6379");
18+
return Redisson.create(config);
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.threestar.trainus.global.utils;
2+
3+
import org.springframework.expression.spel.standard.SpelExpressionParser;
4+
import org.springframework.expression.spel.support.StandardEvaluationContext;
5+
6+
public class CustomSpringELParser {
7+
8+
public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
9+
SpelExpressionParser parser = new SpelExpressionParser();
10+
StandardEvaluationContext context = new StandardEvaluationContext();
11+
12+
for (int i = 0; i < parameterNames.length; i++) {
13+
context.setVariable(parameterNames[i], args[i]);
14+
}
15+
16+
return parser.parseExpression(key).getValue(context, Object.class);
17+
}
18+
19+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.threestar.trainus.global.utils;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.concurrent.TimeUnit;
5+
6+
import org.aspectj.lang.ProceedingJoinPoint;
7+
import org.aspectj.lang.annotation.Around;
8+
import org.aspectj.lang.annotation.Aspect;
9+
import org.aspectj.lang.reflect.MethodSignature;
10+
import org.redisson.api.RLock;
11+
import org.redisson.api.RedissonClient;
12+
import org.springframework.core.annotation.Order;
13+
import org.springframework.stereotype.Component;
14+
15+
import com.threestar.trainus.global.annotation.RedissonLock;
16+
17+
import lombok.RequiredArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
20+
@Slf4j
21+
@Aspect
22+
@Order(1)
23+
@Component
24+
@RequiredArgsConstructor
25+
public class RedssionLockAspect {
26+
27+
private final RedissonClient redissonClient;
28+
29+
@Around("@annotation(com.threestar.trainus.global.annotation.RedissonLock)")
30+
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
31+
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
32+
Method method = signature.getMethod();
33+
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
34+
String lockKey =
35+
method.getName() + CustomSpringELParser.getDynamicValue(signature.getParameterNames(),
36+
joinPoint.getArgs(), annotation.value());
37+
38+
RLock lock = redissonClient.getFairLock(lockKey);
39+
40+
try {
41+
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);
42+
if (!lockable) {
43+
log.info("Lock 획득 실패={}", lockKey);
44+
return null;
45+
}
46+
log.info("로직 수행");
47+
return joinPoint.proceed();
48+
} catch (InterruptedException e) {
49+
log.info("에러 발생");
50+
throw e;
51+
} finally {
52+
if (lock.isHeldByCurrentThread()) {
53+
lock.unlock();
54+
log.info("락 해제 완료={}", lockKey);
55+
}
56+
}
57+
58+
}
59+
}

src/test/java/com/threestar/trainus/coupon/user/UserCouponServiceConcurrencyTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ void tearDown() {
9696
@Test
9797
@DisplayName("동시 요청 시 - 비관적 락 적용: 발급된 쿠폰 수량만큼 발급")
9898
void 쿠폰_동시_발급_테스트() throws InterruptedException {
99-
ExecutorService executor = Executors.newFixedThreadPool(300);
99+
ExecutorService executor = Executors.newFixedThreadPool(100);
100100
CountDownLatch latch = new CountDownLatch(CONCURRENT_USERS);
101101

102102
AtomicInteger successCount = new AtomicInteger();

0 commit comments

Comments
 (0)