Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,8 @@ public int getBatchSize() {
Map<Long, Long> countsByLesson = messages.stream()
.collect(Collectors.groupingBy(ApplyMessage::lessonId, Collectors.counting()));

for (Map.Entry<Long, Long> entry : countsByLesson.entrySet()) {
Long lessonId = entry.getKey();
int amount = entry.getValue().intValue();

int affectedRows = lessonRepository.incrementParticipantCountBatch(lessonId, amount,
LessonStatus.RECRUITMENT_COMPLETED);

if (affectedRows == 0) {
log.error("Batch update failed for lesson [{}]: Capacity exceeded", lessonId);
Metrics.counter("lesson.apply.rejected", "lessonId", String.valueOf(lessonId)).increment(amount);
throw new BusinessException(ErrorCode.LESSON_NOT_AVAILABLE);
}

// Dirty Set 등록
for (Long lessonId : countsByLesson.keySet()) {
// Dirty Set 등록 (스케줄러 보정 요청)
coreRedisTemplate.opsForSet().add(LessonApplyStreamConstant.DIRTY_SET_KEY, String.valueOf(lessonId));
}

Expand Down Expand Up @@ -147,10 +135,12 @@ public boolean apply(Long lessonId, Long userId, String requestId, Long produceT
return false;
}

// 실제 DB 저장 및 카운트 증가 (원자적 연산 적용)
// 실제 DB 저장
LessonParticipant participant = LessonParticipant.builder().lesson(lesson).user(user).build();
lessonParticipantRepository.save(participant);
lessonRepository.incrementParticipantCount(lessonId);

// Dirty Set 등록 (보정 스케줄러 처리 요청)
coreRedisTemplate.opsForSet().add(LessonApplyStreamConstant.DIRTY_SET_KEY, String.valueOf(lessonId));

// 처리 성공 결과 저장 및 지연 시간 측정
long latency = System.currentTimeMillis() - produceTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import java.util.Optional;

import com.threestar.trainus.domain.lesson.teacher.repository.LessonParticipantRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.stream.StreamInfo;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

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

Expand All @@ -24,6 +26,7 @@
public class LessonStockReconciliationScheduler {

private final LessonRepository lessonRepository;
private final LessonParticipantRepository lessonParticipantRepository;
private final LessonApplyProducer lessonApplyProducer;

@Qualifier("coreRedisTemplate")
Expand All @@ -32,6 +35,7 @@ public class LessonStockReconciliationScheduler {
@Qualifier("mqRedisTemplate")
private final StringRedisTemplate mqRedisTemplate;

@Transactional
@Scheduled(fixedRate = 30000)
@SchedulerLock(name = "LessonStockReconciliation", lockAtMostFor = "25s", lockAtLeastFor = "20s")
public void reconcileStock() {
Expand All @@ -41,7 +45,7 @@ public void reconcileStock() {
return;
}

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

String dirtySetKey = LessonApplyStreamConstant.DIRTY_SET_KEY;

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

// DB 카운트 조회
// 실제 참여자 수 계산 (DB)
long actualParticipantCount = lessonParticipantRepository.countByLessonId(lessonId);

// Lesson 테이블 카운트 보정 및 상태 변경
lessonRepository.updateParticipantCount(lessonId, (int)actualParticipantCount,
com.threestar.trainus.domain.lesson.teacher.entity.LessonStatus.RECRUITMENT_COMPLETED);

// 최신 레슨 정보 조회하여 Redis 동기화
Optional<Lesson> lessonOpt = lessonRepository.findById(lessonId);
if (lessonOpt.isPresent()) {
Lesson lesson = lessonOpt.get();
Expand All @@ -74,7 +85,8 @@ public void reconcileStock() {
coreRedisTemplate.opsForSet().remove(dirtySetKey, lessonIdStr);

processedCount++;
log.debug("Reconciled lesson [{}] stock to [{}]", lessonId, currentStock);
log.debug("Reconciled lesson [{}] to actual count [{}] and stock [{}]",
lessonId, actualParticipantCount, currentStock);
}
} catch (Exception e) {
log.error("Failed to reconcile lesson [{}]: {}", lessonIdStr, e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ default void decrementParticipantCount(Long lessonId) {
updateDecrementInternal(lessonId, LessonStatus.RECRUITING);
}

@Modifying(clearAutomatically = true)
@Query("""
UPDATE Lesson l
SET l.participantCount = :count,
l.status = CASE WHEN :count >= l.maxParticipants
THEN :completedStatus
ELSE l.status END
WHERE l.id = :lessonId
""")
int updateParticipantCount(
@Param("lessonId") Long lessonId,
@Param("count") int count,
@Param("completedStatus") LessonStatus completedStatus
);

// 중복 레슨 검증(같은 강사가 같은 이름과 시작시간으로 레슨 생성했는지 체크)
@Query("""
SELECT COUNT(l) > 0 FROM Lesson l
Expand Down
Loading