Skip to content

Commit 15fd76c

Browse files
authored
[Infra] 선착순 시스템 성능 최적화 검증을 위한 레거시 환경 구축 (#218)
* feat: 성능 비교 테스트를 위한 레거시 모드 도입 및 스케줄러 안정화
1 parent b71a76c commit 15fd76c

5 files changed

Lines changed: 152 additions & 9 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.threestar.trainus.domain.lesson.issue;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
import org.springframework.beans.factory.annotation.Qualifier;
9+
import org.springframework.context.annotation.Profile;
10+
import org.springframework.data.redis.core.StringRedisTemplate;
11+
import org.springframework.scheduling.annotation.Scheduled;
12+
import org.springframework.stereotype.Component;
13+
14+
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
16+
17+
@Profile("consumer & legacy")
18+
@Slf4j
19+
@Component
20+
@RequiredArgsConstructor
21+
public class LegacyLessonAdmissionScheduler {
22+
23+
@Qualifier("coreRedisTemplate")
24+
private final StringRedisTemplate coreRedisTemplate;
25+
26+
@Qualifier("mqRedisTemplate")
27+
private final StringRedisTemplate mqRedisTemplate;
28+
29+
private final LessonWaitingRoomService waitingRoomService;
30+
31+
private static final long ADMIT_BATCH_SIZE = 1000L;
32+
33+
@Scheduled(fixedDelay = 500)
34+
public void admitUsers() {
35+
Set<String> activeLessonIds = coreRedisTemplate.opsForSet().members(LessonApplyStreamConstant.DIRTY_SET_KEY);
36+
37+
if (activeLessonIds == null || activeLessonIds.isEmpty()) {
38+
return;
39+
}
40+
41+
for (String lessonIdStr : activeLessonIds) {
42+
Long lessonId = Long.parseLong(lessonIdStr);
43+
processAdmissionForLesson(lessonId);
44+
}
45+
}
46+
47+
private void processAdmissionForLesson(Long lessonId) {
48+
// 해당 레슨 대기열에서 인원 추출
49+
Set<String> requestIds = waitingRoomService.dequeue(lessonId, ADMIT_BATCH_SIZE);
50+
51+
if (requestIds.isEmpty()) {
52+
return;
53+
}
54+
55+
// MGET 방식으로 벌크 GET
56+
List<String> statusKeys = requestIds.stream().map(id -> LessonApplyStreamConstant.STATUS_PREFIX + id).toList();
57+
List<String> statusInfos = mqRedisTemplate.opsForValue().multiGet(statusKeys);
58+
59+
if (statusInfos == null)
60+
return;
61+
62+
// 상태 변경 및 스트림 추가
63+
for (int i = 0; i < statusKeys.size(); i++) {
64+
String info = statusInfos.get(i);
65+
if (info == null)
66+
continue;
67+
68+
String[] parts = info.split(":");
69+
if (parts.length < 3)
70+
continue;
71+
72+
String requestId = requestIds.toArray(new String[0])[i];
73+
Long userId = Long.parseLong(parts[2]);
74+
Long originalTimestamp = parts.length >= 4 ? Long.parseLong(parts[3]) : System.currentTimeMillis();
75+
76+
// 상태 변경
77+
String statusKey = statusKeys.get(i);
78+
String processingInfo = String.format("%s:%d:%d:%d", LessonApplyStreamConstant.STATUS_PROCESSING, lessonId,
79+
userId, originalTimestamp);
80+
mqRedisTemplate.opsForValue()
81+
.set(statusKey, processingInfo,
82+
java.time.Duration.ofMinutes(LessonApplyStreamConstant.STATUS_TTL_MINUTE));
83+
84+
// 스트림 추가
85+
Map<String, String> content = new HashMap<>();
86+
content.put("lessonId", String.valueOf(lessonId));
87+
content.put("userId", String.valueOf(userId));
88+
content.put("requestId", requestId);
89+
content.put("timestamp", String.valueOf(originalTimestamp));
90+
mqRedisTemplate.opsForStream().add(LessonApplyStreamConstant.STREAM_KEY, content);
91+
}
92+
93+
log.info("[Legacy] Admitted {} users to MQ via iteration for lesson: {}", requestIds.size(), lessonId);
94+
}
95+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.threestar.trainus.domain.lesson.issue;
2+
3+
import org.springframework.beans.factory.annotation.Qualifier;
4+
import org.springframework.context.annotation.Profile;
5+
import org.springframework.data.redis.connection.stream.MapRecord;
6+
import org.springframework.data.redis.core.StringRedisTemplate;
7+
import org.springframework.data.redis.stream.StreamListener;
8+
import org.springframework.stereotype.Component;
9+
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
13+
import java.util.Map;
14+
15+
@Profile("consumer & legacy")
16+
@Slf4j
17+
@Component
18+
@RequiredArgsConstructor
19+
public class LegacyLessonApplyConsumer implements StreamListener<String, MapRecord<String, String, String>> {
20+
21+
@Qualifier("mqRedisTemplate")
22+
private final StringRedisTemplate mqRedisTemplate;
23+
24+
private final LessonApplyService lessonApplyService;
25+
26+
private static final String STREAM_KEY = LessonApplyStreamConstant.STREAM_KEY;
27+
private static final String GROUP = LessonApplyStreamConstant.GROUP;
28+
29+
@Override
30+
public void onMessage(MapRecord<String, String, String> message) {
31+
Map<String, String> value = message.getValue();
32+
33+
Long lessonId = Long.parseLong(value.get("lessonId"));
34+
Long userId = Long.parseLong(value.get("userId"));
35+
String requestId = value.get("requestId");
36+
Long timestamp = Long.parseLong(value.get("timestamp"));
37+
38+
try {
39+
// DB 저장
40+
lessonApplyService.apply(lessonId, userId, requestId, timestamp);
41+
42+
// Redis ACK 및 DELETE
43+
mqRedisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, message.getId());
44+
mqRedisTemplate.opsForStream().delete(STREAM_KEY, message.getId());
45+
46+
log.info("[Legacy] Consumer processed single message: {}", requestId);
47+
} catch (Exception e) {
48+
log.error("[Legacy] Consumer failed: {}. Error: {}", requestId, e.getMessage());
49+
mqRedisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, message.getId());
50+
mqRedisTemplate.opsForStream().delete(STREAM_KEY, message.getId());
51+
}
52+
}
53+
}

src/main/java/com/threestar/trainus/domain/lesson/issue/LessonAdmissionScheduler.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import lombok.RequiredArgsConstructor;
1717
import lombok.extern.slf4j.Slf4j;
1818

19-
@Profile("consumer")
19+
@Profile("consumer & !legacy")
2020
@Slf4j
2121
@Component
2222
@RequiredArgsConstructor
@@ -59,12 +59,6 @@ private void processAdmissionForLesson(Long lessonId) {
5959
Set<String> requestIds = waitingRoomService.dequeue(lessonId, ADMIT_BATCH_SIZE);
6060

6161
if (requestIds.isEmpty()) {
62-
// 실제 대기열이 비었는지 다시 확인
63-
Long remainingInWaitingRoom = coreRedisTemplate.opsForZSet().size(String.format(LessonApplyStreamConstant.WAITING_ROOM_KEY, lessonId));
64-
if (remainingInWaitingRoom == null || remainingInWaitingRoom == 0) {
65-
coreRedisTemplate.opsForSet().remove(LessonApplyStreamConstant.DIRTY_SET_KEY, String.valueOf(lessonId));
66-
log.info("Lesson {} admission completed. Removed from active set.", lessonId);
67-
}
6862
return;
6963
}
7064

src/main/java/com/threestar/trainus/domain/lesson/issue/LessonApplyConsumer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import java.util.concurrent.ConcurrentLinkedQueue;
1919
import java.util.stream.Collectors;
2020

21-
@Profile("consumer")
21+
@Profile("consumer & !legacy")
2222
@Slf4j
2323
@Component
2424
@RequiredArgsConstructor

src/main/java/com/threestar/trainus/domain/lesson/issue/LessonApplyStreamConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.data.redis.connection.stream.ReadOffset;
1212
import org.springframework.data.redis.connection.stream.StreamOffset;
1313
import org.springframework.data.redis.core.StringRedisTemplate;
14+
import org.springframework.data.redis.stream.StreamListener;
1415
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
1516

1617
import jakarta.annotation.PostConstruct;
@@ -27,7 +28,7 @@ public class LessonApplyStreamConfig {
2728
private int concurrency;
2829

2930
private final StreamMessageListenerContainer<String, MapRecord<String, String, String>> container;
30-
private final LessonApplyConsumer lessonApplyConsumer;
31+
private final StreamListener<String, MapRecord<String, String, String>> lessonApplyConsumer;
3132

3233
@Qualifier("mqRedisTemplate")
3334
private final StringRedisTemplate mqRedisTemplate;

0 commit comments

Comments
 (0)