Skip to content

Commit

Permalink
Merge pull request #105 from softeerbootcamp4th/feature/101-refactor-…
Browse files Browse the repository at this point in the history
…fcfs

[refactor] 선착순 이벤트 API 매개변수 eventSequenced에서 eventId로 변경 (#101)
  • Loading branch information
win-luck authored Aug 20, 2024
2 parents cf6eb4f + 8f6e981 commit 71a3da4
Show file tree
Hide file tree
Showing 18 changed files with 226 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,39 @@
import hyundai.softeer.orange.common.util.ConstantUtil;
import hyundai.softeer.orange.event.common.repository.EventFrameRepository;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
@Component
public class CommentScheduler {

// 분산 환경에서 메서드가 여러 번 실행되는 것을 방지하기 위해 분산 락 도입
private final RedissonClient redissonClient;
private final CommentService commentService;
private final EventFrameRepository eventFrameRepository;

// 스케줄러에 의해 일정 시간마다 캐싱된 긍정 기대평 목록을 초기화한다.
@Scheduled(fixedRate = ConstantUtil.SCHEDULED_TIME) // 2시간마다 실행
private void clearCache() {
List<String> frameIds = eventFrameRepository.findAllFrameIds();
frameIds.forEach(commentService::getComments);
private void clearCache() throws InterruptedException {
RLock lock = redissonClient.getLock(ConstantUtil.DB_TO_REDIS_LOCK);
try {
// 5분동안 락 점유
if (lock.tryLock(0, 5, TimeUnit.MINUTES)) {
try {
List<String> frameIds = eventFrameRepository.findAllFrameIds();
frameIds.forEach(commentService::getComments);
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class ConstantUtil {
public static final String WAITING = "waiting";
public static final String PROGRESS = "progress";

public static final String DB_TO_REDIS_LOCK = "FCFS_MANAGE_DB_TO_REDIS";
public static final String REDIS_TO_DB_LOCK = "FCFS_MANAGE_REDIS_TO_DB";

public static final double LIMIT_NEGATIVE_CONFIDENCE = 99.5;
public static final int COMMENTS_SIZE = 20;
public static final int SCHEDULED_TIME = 1000 * 60 * 60 * 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,39 +33,39 @@ public class FcfsController {
private final FcfsManageService fcfsManageService;

@EventUserAuthRequirement @Auth(AuthRole.event_user)
@PostMapping("/{eventSequence}")
@PostMapping("/{eventId}")
@Operation(summary = "선착순 이벤트 참여", description = "선착순 이벤트에 참여한 결과(boolean)를 반환한다.", responses = {
@ApiResponse(responseCode = "200", description = "선착순 이벤트 당첨 성공 혹은 실패",
content = @Content(schema = @Schema(implementation = ResponseFcfsResultDto.class))),
@ApiResponse(responseCode = "400", description = "선착순 이벤트 시간이 아니거나, 요청 형식이 잘못된 경우",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<ResponseFcfsResultDto> participate(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable Long eventSequence, @RequestBody RequestAnswerDto dto) {
boolean answerResult = fcfsAnswerService.judgeAnswer(eventSequence, dto.getAnswer());
boolean isWin = answerResult && fcfsService.participate(eventSequence, userInfo.getUserId());
public ResponseEntity<ResponseFcfsResultDto> participate(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventId, @RequestBody RequestAnswerDto dto) {
boolean answerResult = fcfsAnswerService.judgeAnswer(eventId, dto.getAnswer());
boolean isWin = answerResult && fcfsService.participate(eventId, userInfo.getUserId());
return ResponseEntity.ok(new ResponseFcfsResultDto(answerResult, isWin));
}

@GetMapping("/{eventSequence}/info")
@GetMapping("/{eventId}/info")
@Operation(summary = "특정 선착순 이벤트의 정보 조회", description = "특정 선착순 이벤트에 대한 정보(서버 기준 시각, 이벤트의 상태)를 반환한다.", responses = {
@ApiResponse(responseCode = "200", description = "선착순 이벤트에 대한 상태 정보",
content = @Content(schema = @Schema(implementation = ResponseFcfsInfoDto.class))),
@ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<ResponseFcfsInfoDto> getFcfsInfo(@PathVariable Long eventSequence) {
return ResponseEntity.ok(fcfsManageService.getFcfsInfo(eventSequence));
public ResponseEntity<ResponseFcfsInfoDto> getFcfsInfo(@PathVariable String eventId) {
return ResponseEntity.ok(fcfsManageService.getFcfsInfo(eventId));
}

@EventUserAuthRequirement @Auth(AuthRole.event_user)
@GetMapping("/{eventSequence}/participated")
@GetMapping("/{eventId}/participated")
@Operation(summary = "선착순 이벤트 참여 여부 조회", description = "정답을 맞혀서 선착순 이벤트에 참여했는지 여부를 조회한다. (당첨은 별도)", responses = {
@ApiResponse(responseCode = "200", description = "선착순 이벤트의 정답을 맞혀서 참여했는지에 대한 결과",
content = @Content(schema = @Schema(implementation = ResponseFcfsResultDto.class))),
@ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<Boolean> isParticipated(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable Long eventSequence) {
return ResponseEntity.ok(fcfsManageService.isParticipated(eventSequence, userInfo.getUserId()));
public ResponseEntity<Boolean> isParticipated(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventId) {
return ResponseEntity.ok(fcfsManageService.isParticipated(eventId, userInfo.getUserId()));
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
package hyundai.softeer.orange.event.fcfs.scheduler;

import hyundai.softeer.orange.common.util.ConstantUtil;
import hyundai.softeer.orange.event.fcfs.service.FcfsManageService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
@Component
public class FcfsScheduler {

// 분산 환경에서 메서드가 여러 번 실행되는 것을 방지하기 위해 분산 락 도입
private final RedissonClient redissonClient;
private final FcfsManageService fcfsManageService;

// 매일 자정 1분마다 실행되며, 오늘의 선착순 이벤트에 대한 정보를 DB에서 Redis로 이동시킨다.
@Scheduled(cron = "0 1 0 * * *")
public void registerFcfsEvents() {
fcfsManageService.registerFcfsEvents();
RLock lock = redissonClient.getLock(ConstantUtil.DB_TO_REDIS_LOCK);
try {
// 5분동안 락 점유
if (lock.tryLock(0, 5, TimeUnit.MINUTES)) {
try {
fcfsManageService.registerFcfsEvents();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// 매일 자정마다 실행되며, 선착순 이벤트 당첨자들을 Redis에서 DB로 이동시킨다.
@Scheduled(cron = "0 0 0 * * *")
public void registerWinners() {
fcfsManageService.registerWinners();
RLock lock = redissonClient.getLock(ConstantUtil.REDIS_TO_DB_LOCK);
try {
// 5분동안 락 점유
if (lock.tryLock(0, 5, TimeUnit.MINUTES)) {
try {
fcfsManageService.registerWinners();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// FIXME: 빌드 직후 오늘의 선착순 이벤트에 대한 정보를 DB에서 Redis로 이동시킨다. (추후 삭제예정)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -26,11 +27,17 @@ public class DbFcfsService implements FcfsService{
private final EventUserRepository eventUserRepository;
private final FcfsEventWinningInfoRepository fcfsEventWinningInfoRepository;
private final RedisTemplate<String, Boolean> booleanRedisTemplate;
private final StringRedisTemplate stringRedisTemplate;

@Override
@Transactional
public boolean participate(Long eventSequence, String userId){
String key = eventSequence.toString();
public boolean participate(String eventId, String userId){
String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId));
if(key == null) {
throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND);
}
Long eventSequence = Long.parseLong(key);

// 이벤트 종료 여부 확인
if (isEventEnded(key)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ public class FcfsAnswerService {

private final StringRedisTemplate stringRedisTemplate;

public boolean judgeAnswer(Long eventSequence, String answer) {
public boolean judgeAnswer(String eventId, String answer) {
// eventId로부터 FCFS의 key를 가져옴
String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId));
if(key == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}

// 잘못된 이벤트 참여 시간
String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString()));
String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(key));
if(startTime == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}
Expand All @@ -26,7 +32,7 @@ public boolean judgeAnswer(Long eventSequence, String answer) {
}

// 정답 비교
String correctAnswer = stringRedisTemplate.opsForValue().get(FcfsUtil.answerFormatting(eventSequence.toString()));
String correctAnswer = stringRedisTemplate.opsForValue().get(FcfsUtil.answerFormatting(key));
if (correctAnswer == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,21 @@ public void registerWinners() {
fcfsEventWinningInfoRepository.saveAll(winningInfos);
deleteEventInfo(eventId);
}

Set<String> eventIds = stringRedisTemplate.keys("*:eventId");
if(eventIds != null && !eventIds.isEmpty()) {
for(String eventId : eventIds) {
stringRedisTemplate.delete(eventId);
}
}
log.info("Winners of all FCFS events were registered in DB");
}

// 특정 선착순 이벤트의 정보 조회
public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) {
String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString()));
public ResponseFcfsInfoDto getFcfsInfo(String eventId) {
String key = getFcfsKeyFromEventId(eventId);

String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(key));
// 선착순 이벤트가 존재하지 않는 경우
if (startTime == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
Expand All @@ -91,7 +100,7 @@ public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) {
// 서버시간 < 이벤트시작시간 < 서버시간+3시간 -> countdown
// 이벤트시작시간 < 서버시간 < 이벤트시작시간+7시간 -> progress
// 그 외 -> waiting
log.info("Checked FCFS event status: {}", eventSequence);
log.info("Checked FCFS event status: {}", key);
if(nowDateTime.isBefore(eventStartTime) && nowDateTime.plusHours(ConstantUtil.FCFS_COUNTDOWN_HOUR).isAfter(eventStartTime)) {
return new ResponseFcfsInfoDto(eventStartTime, ConstantUtil.COUNTDOWN);
} else if(eventStartTime.isBefore(nowDateTime) && eventStartTime.plusHours(ConstantUtil.FCFS_AVAILABLE_HOUR).isAfter(nowDateTime)) {
Expand All @@ -103,11 +112,12 @@ public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) {

// 특정 유저가 선착순 이벤트의 참여자인지 조회 (정답을 맞힌 경우 참여자로 간주)
@Transactional(readOnly = true)
public Boolean isParticipated(Long eventSequence, String userId) {
if(!fcfsEventRepository.existsById(eventSequence)) {
public Boolean isParticipated(String eventId, String userId) {
String key = getFcfsKeyFromEventId(eventId);
if(!fcfsEventRepository.existsById(Long.parseLong(key))) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}
return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(eventSequence.toString()), userId));
return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(key), userId));
}

// 특정 선착순 이벤트의 당첨자 조회 - 어드민에서 사용
Expand All @@ -126,6 +136,7 @@ public List<ResponseFcfsWinnerDto> getFcfsWinnersInfo(Long eventSequence) {

private void prepareEventInfo(FcfsEvent event) {
String key = event.getId().toString();
stringRedisTemplate.opsForValue().set(FcfsUtil.eventIdFormatting(event.getEventMetaData().getEventId()), key);
numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(key), event.getParticipantCount().intValue());
booleanRedisTemplate.opsForValue().set(FcfsUtil.endFlagFormatting(key), false);
stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(key), event.getStartTime().toString());
Expand Down Expand Up @@ -154,4 +165,12 @@ private LocalDateTime getTimeFromScore(Double score) {
long timeMillis = score.longValue();
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), ZoneId.systemDefault());
}

private String getFcfsKeyFromEventId(String eventId) {
String key = stringRedisTemplate.opsForValue().get(eventId);
if(key == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}
return key;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package hyundai.softeer.orange.event.fcfs.service;

public interface FcfsService {
boolean participate(Long eventSequence, String userId);
boolean participate(String eventId, String userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ public class RedisLockFcfsService implements FcfsService {
private final RedisTemplate<String, Integer> numberRedisTemplate;

@Override
public boolean participate(Long eventSequence, String userId) {
String key = eventSequence.toString();
public boolean participate(String eventId, String userId) {
String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId));
if(key == null) {
throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND);
}
Long eventSequence = Long.parseLong(key);

// 이벤트 종료 여부 확인
if (isEventEnded(key)) {
stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ public class RedisLuaFcfsService implements FcfsService {
private final RedisTemplate<String, Boolean> booleanRedisTemplate;

@Override
public boolean participate(Long eventSequence, String userId) {
String key = eventSequence.toString();
public boolean participate(String eventId, String userId) {
String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId));
if(key == null) {
throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND);
}
Long eventSequence = Long.parseLong(key);

// 이벤트 종료 여부 확인
if (isEventEnded(key)) {
stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ public class RedisSetFcfsService implements FcfsService {
private final StringRedisTemplate stringRedisTemplate;

@Override
public boolean participate(Long eventSequence, String userId) {
String key = eventSequence.toString();
public boolean participate(String eventId, String userId) {
String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId));
if(key == null) {
throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND);
}
Long eventSequence = Long.parseLong(key);

// 이벤트 종료 여부 확인
if(isEventEnded(key)) {
stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,34 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FcfsUtil {

// 선착순 이벤트의 PK를 간접적으로 보관하는 eventId tag
public static String eventIdFormatting(String key) {
return key + ":eventId";
}

// 선착순 이벤트 tag
public static String keyFormatting(String key) {
return key + ":fcfs";
}

// 선착순 이벤트 시작 시각 tag
public static String startTimeFormatting(String key) {
return key + "_start";
return key + ":start";
}

// 선착순 이벤트 마감 여부 tag
public static String endFlagFormatting(String key) {
return key + "_end";
return key + ":end";
}

// 선착순 이벤트 당첨자 tag
public static String winnerFormatting(String key) {
return key + "_winner";
return key + ":winner";
}

// 선착순 이벤트 참여자 tag
public static String participantFormatting(String key) {
return key + "_participant";
return key + ":participant";
}

// 선착순 이벤트 정답 tag
Expand Down
Loading

0 comments on commit 71a3da4

Please sign in to comment.