Skip to content

Commit 74c8bf4

Browse files
authored
refactor: Row Lock 경합 제거를 위한 레슨 참가자 수 업데이트 로직 디커플링 (#222)
1 parent e889a97 commit 74c8bf4

3 files changed

Lines changed: 36 additions & 19 deletions

File tree

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

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,8 @@ public int getBatchSize() {
8585
Map<Long, Long> countsByLesson = messages.stream()
8686
.collect(Collectors.groupingBy(ApplyMessage::lessonId, Collectors.counting()));
8787

88-
for (Map.Entry<Long, Long> entry : countsByLesson.entrySet()) {
89-
Long lessonId = entry.getKey();
90-
int amount = entry.getValue().intValue();
91-
92-
int affectedRows = lessonRepository.incrementParticipantCountBatch(lessonId, amount,
93-
LessonStatus.RECRUITMENT_COMPLETED);
94-
95-
if (affectedRows == 0) {
96-
log.error("Batch update failed for lesson [{}]: Capacity exceeded", lessonId);
97-
Metrics.counter("lesson.apply.rejected", "lessonId", String.valueOf(lessonId)).increment(amount);
98-
throw new BusinessException(ErrorCode.LESSON_NOT_AVAILABLE);
99-
}
100-
101-
// Dirty Set 등록
88+
for (Long lessonId : countsByLesson.keySet()) {
89+
// Dirty Set 등록 (스케줄러 보정 요청)
10290
coreRedisTemplate.opsForSet().add(LessonApplyStreamConstant.DIRTY_SET_KEY, String.valueOf(lessonId));
10391
}
10492

@@ -147,10 +135,12 @@ public boolean apply(Long lessonId, Long userId, String requestId, Long produceT
147135
return false;
148136
}
149137

150-
// 실제 DB 저장 및 카운트 증가 (원자적 연산 적용)
138+
// 실제 DB 저장
151139
LessonParticipant participant = LessonParticipant.builder().lesson(lesson).user(user).build();
152140
lessonParticipantRepository.save(participant);
153-
lessonRepository.incrementParticipantCount(lessonId);
141+
142+
// Dirty Set 등록 (보정 스케줄러 처리 요청)
143+
coreRedisTemplate.opsForSet().add(LessonApplyStreamConstant.DIRTY_SET_KEY, String.valueOf(lessonId));
154144

155145
// 처리 성공 결과 저장 및 지연 시간 측정
156146
long latency = System.currentTimeMillis() - produceTime;

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import java.util.Optional;
44

5+
import com.threestar.trainus.domain.lesson.teacher.repository.LessonParticipantRepository;
56
import org.springframework.beans.factory.annotation.Qualifier;
67
import org.springframework.context.annotation.Profile;
78
import org.springframework.data.redis.connection.stream.StreamInfo;
89
import org.springframework.data.redis.core.StringRedisTemplate;
910
import org.springframework.scheduling.annotation.Scheduled;
1011
import org.springframework.stereotype.Component;
12+
import org.springframework.transaction.annotation.Transactional;
1113

1214
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
1315

@@ -24,6 +26,7 @@
2426
public class LessonStockReconciliationScheduler {
2527

2628
private final LessonRepository lessonRepository;
29+
private final LessonParticipantRepository lessonParticipantRepository;
2730
private final LessonApplyProducer lessonApplyProducer;
2831

2932
@Qualifier("coreRedisTemplate")
@@ -32,6 +35,7 @@ public class LessonStockReconciliationScheduler {
3235
@Qualifier("mqRedisTemplate")
3336
private final StringRedisTemplate mqRedisTemplate;
3437

38+
@Transactional
3539
@Scheduled(fixedRate = 30000)
3640
@SchedulerLock(name = "LessonStockReconciliation", lockAtMostFor = "25s", lockAtLeastFor = "20s")
3741
public void reconcileStock() {
@@ -41,7 +45,7 @@ public void reconcileStock() {
4145
return;
4246
}
4347

44-
log.info("Starting Smart Stock Reconciliation (DB -> Redis)...");
48+
log.info("Starting Smart Stock Reconciliation (Actual DB Count -> Lesson Row -> Redis)...");
4549

4650
String dirtySetKey = LessonApplyStreamConstant.DIRTY_SET_KEY;
4751

@@ -57,7 +61,14 @@ public void reconcileStock() {
5761
try {
5862
Long lessonId = Long.valueOf(lessonIdStr);
5963

60-
// DB 카운트 조회
64+
// 실제 참여자 수 계산 (DB)
65+
long actualParticipantCount = lessonParticipantRepository.countByLessonId(lessonId);
66+
67+
// Lesson 테이블 카운트 보정 및 상태 변경
68+
lessonRepository.updateParticipantCount(lessonId, (int)actualParticipantCount,
69+
com.threestar.trainus.domain.lesson.teacher.entity.LessonStatus.RECRUITMENT_COMPLETED);
70+
71+
// 최신 레슨 정보 조회하여 Redis 동기화
6172
Optional<Lesson> lessonOpt = lessonRepository.findById(lessonId);
6273
if (lessonOpt.isPresent()) {
6374
Lesson lesson = lessonOpt.get();
@@ -74,7 +85,8 @@ public void reconcileStock() {
7485
coreRedisTemplate.opsForSet().remove(dirtySetKey, lessonIdStr);
7586

7687
processedCount++;
77-
log.debug("Reconciled lesson [{}] stock to [{}]", lessonId, currentStock);
88+
log.debug("Reconciled lesson [{}] to actual count [{}] and stock [{}]",
89+
lessonId, actualParticipantCount, currentStock);
7890
}
7991
} catch (Exception e) {
8092
log.error("Failed to reconcile lesson [{}]: {}", lessonIdStr, e.getMessage());

src/main/java/com/threestar/trainus/domain/lesson/teacher/repository/LessonRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ default void decrementParticipantCount(Long lessonId) {
8080
updateDecrementInternal(lessonId, LessonStatus.RECRUITING);
8181
}
8282

83+
@Modifying(clearAutomatically = true)
84+
@Query("""
85+
UPDATE Lesson l
86+
SET l.participantCount = :count,
87+
l.status = CASE WHEN :count >= l.maxParticipants
88+
THEN :completedStatus
89+
ELSE l.status END
90+
WHERE l.id = :lessonId
91+
""")
92+
int updateParticipantCount(
93+
@Param("lessonId") Long lessonId,
94+
@Param("count") int count,
95+
@Param("completedStatus") LessonStatus completedStatus
96+
);
97+
8398
// 중복 레슨 검증(같은 강사가 같은 이름과 시작시간으로 레슨 생성했는지 체크)
8499
@Query("""
85100
SELECT COUNT(l) > 0 FROM Lesson l

0 commit comments

Comments
 (0)