From 0ac10699b421869b4bbc203d82706b0ac0a51fbb Mon Sep 17 00:00:00 2001 From: Hanna Lee <8annahxxl@gmail.com> Date: Wed, 17 Jan 2024 00:05:34 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8C=80=EA=B8=B0=EC=97=B4,=20=EC=B0=B8?= =?UTF-8?q?=EA=B0=80=EC=97=B4=20=EB=B6=84=EB=A6=AC=20(#250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 참가열 zset 분리 * faet: 대기열 큐 만료시간 설정 로직 추가 --- .DS_Store | Bin 0 -> 6148 bytes api/api-booking/.DS_Store | Bin 0 -> 6148 bytes api/api-booking/http/booking.http | 2 +- .../apibooking/common/util/RedisOperator.java | 17 +++++ .../com/pgms/apibooking/config/WebConfig.java | 2 +- .../booking/service/BookingService.java | 2 +- .../controller/BookingQueueController.java | 10 +-- .../service/BookingQueueManager.java | 70 +++++++++++++++--- .../service/BookingQueueService.java | 68 +++++++++++------ .../domain/seat/service/SeatLockManager.java | 10 +-- .../service/BookingServiceTest.java | 2 +- http/bingterpark.http | 2 +- 12 files changed, 139 insertions(+), 46 deletions(-) create mode 100644 .DS_Store create mode 100644 api/api-booking/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..43c3cf2168ae9bc8c7e3d569267f9d67bbf1a637 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8Nhm@N3Oz1(Ef`BK#Y>3w1&ruHr6#0kFlI}W+CwSitS{t~_&m<+ zZop#BB6bFLzxmzGevtiPjPY=po-*b##%ySa9F-P9cWbC)k`XzMQO=WGCSZLea})dP zfZyI_F`Kh2S$+ThJS!{D`{bQ&Z*SkS9IJ2LM^ADURdKZ}!+3Fv)|Hfb(&%w=oy}HL z_wZa6Rh$*Gg(}F>8Km6ZWJM}hp+#3~eLUK1 z9Q)|_m{Q9#GH1fnn)7_2oy1cd8SKwZks6NBq= z@C%a{7_2qwa>munFpinIdAx8nJNSi4XFSkIJuyHGY%|c+rib1Mg;`)txEtHxQ`4}P{#%8kQW%NHR34fSLJ|o N5m1CsM-2P|179x?Nn-#2 literal 0 HcmV?d00001 diff --git a/api/api-booking/.DS_Store b/api/api-booking/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..55eb0cce4f47364913e3ece56d5893708136f405 GIT binary patch literal 6148 zcmeHK%}T>S5Z<+|Nhm@N3OxqA7K|mA;w8lT0!H+pQWH`%7_+5G?4cBL))(?gd>&_Z zH_&46C}L+|_nV!c-OLBsAI2CDs^pY0n=xiWL*%Hm2%1}6dpa1A%Q=#GS)nF$gggRp27Z~^g9id9b literal 0 HcmV?d00001 diff --git a/api/api-booking/http/booking.http b/api/api-booking/http/booking.http index 3fd23411..44eab5cd 100644 --- a/api/api-booking/http/booking.http +++ b/api/api-booking/http/booking.http @@ -13,7 +13,7 @@ Booking-Session-Id: {{sessionId}} } ### 대기열 조회 -GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1 +GET http://localhost:8082/api/v1/bookings/waiting-order?eventId=1 Authorization: Bearer {{accessToken}} Booking-Session-Id: {{sessionId}} diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/common/util/RedisOperator.java b/api/api-booking/src/main/java/com/pgms/apibooking/common/util/RedisOperator.java index 3512d8e6..865751b8 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/common/util/RedisOperator.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/common/util/RedisOperator.java @@ -19,6 +19,15 @@ public class RedisOperator { private final RedisTemplate redisTemplate; + public Boolean exists(String key) { + return tryOperation(() -> redisTemplate.hasKey(key)); + } + + public void expire(String key, long expirationSeconds) { + Duration timeout = Duration.ofSeconds(expirationSeconds); + tryOperation(() -> redisTemplate.expire(key, timeout)); + } + public void setIfAbsent(String key, String value, Integer expirationSeconds) { Duration timeout = Duration.ofSeconds(expirationSeconds); tryOperation(() -> redisTemplate.opsForValue().setIfAbsent(key, value, timeout)); @@ -36,10 +45,18 @@ public void addToZSet(String key, String value, double score) { tryOperation(() -> redisTemplate.opsForZSet().add(key, value, score)); } + public Long getSizeOfZSet(String key) { + return tryOperation(() -> redisTemplate.opsForZSet().size(key)); + } + public Long getRankFromZSet(String key, String value) { return tryOperation(() -> redisTemplate.opsForZSet().rank(key, value)); } + public Double getScoreFromZSet(String key, String index) { + return tryOperation(() -> redisTemplate.opsForZSet().score(key, index)); + } + public void removeElementFromZSet(String key, String value) { tryOperation(() -> redisTemplate.opsForZSet().remove(key, value)); } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/config/WebConfig.java b/api/api-booking/src/main/java/com/pgms/apibooking/config/WebConfig.java index 01894e88..19b992df 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/config/WebConfig.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/config/WebConfig.java @@ -22,7 +22,7 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(bookingSessionInterceptor) .addPathPatterns("/api/*/bookings/enter-queue") - .addPathPatterns("/api/*/bookings/order-in-queue") + .addPathPatterns("/api/*/bookings/waiting-order") .addPathPatterns("/api/*/bookings/issue-token") .addPathPatterns("/api/*/bookings/exit-queue"); registry.addInterceptor(bookingTokenInterceptor) diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java index f93d0e80..4cbe182f 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java @@ -174,7 +174,7 @@ public BookingGetResponse getBooking(String id, Long memberId) { @Async protected void removeSessionIdInBookingQueue(Long eventId, String tokenSessionId) { - bookingQueueManager.remove(eventId, tokenSessionId); + bookingQueueManager.removeFromParticipantQueue(eventId, tokenSessionId); } private EventTime getBookableTimeWithEvent(Long timeId) { diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/controller/BookingQueueController.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/controller/BookingQueueController.java index 7fb7ffbf..3055a89a 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/controller/BookingQueueController.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/controller/BookingQueueController.java @@ -40,16 +40,16 @@ public ResponseEntity> issueSessionId() { @Operation(summary = "대기열 진입") @PostMapping("/enter-queue") - public ResponseEntity enterQueue(@RequestBody @Valid BookingQueueEnterRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) { - bookingQueueService.enterQueue(request, bookingSessionId); + public ResponseEntity enterWaitingQueue(@RequestBody @Valid BookingQueueEnterRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) { + bookingQueueService.enterWaitingQueue(request, bookingSessionId); return ResponseEntity.noContent().build(); } @Operation(summary = "내 대기 순서 확인") - @GetMapping("/order-in-queue") - public ResponseEntity> getOrderInQueue(@RequestParam Long eventId, @RequestAttribute("bookingSessionId") String bookingSessionId) { + @GetMapping("/waiting-order") + public ResponseEntity> getWaitingOrder(@RequestParam Long eventId, @RequestAttribute("bookingSessionId") String bookingSessionId) { ApiResponse response = - ApiResponse.ok(bookingQueueService.getOrderInQueue(eventId, bookingSessionId)); + ApiResponse.ok(bookingQueueService.getWaitingOrder(eventId, bookingSessionId)); return ResponseEntity.ok(response); } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueManager.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueManager.java index b60d26d3..f62c61e0 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueManager.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueManager.java @@ -12,22 +12,74 @@ @RequiredArgsConstructor public class BookingQueueManager { + private static final String WAITING_QUEUE_KEY_PREFIX = "waiting:eventId:"; + private static final String PARTICIPANT_QUEUE_KEY_PREFIX = "participant:eventId:"; + private static final long QUEUE_TIMEOUT_SECONDS = 60 * 24 * 60 * 60; // 2 months private final RedisOperator redisOperator; - public void add(Long eventId, String sessionId, double currentTimeSeconds) { - redisOperator.addToZSet(String.valueOf(eventId), sessionId, currentTimeSeconds); + public void addToWaitingQueue(Long eventId, String sessionId, double currentTimeSeconds) { + String key = generateWaitingQueueKey(eventId); + + if (redisOperator.exists(key)) { + redisOperator.addToZSet(key, sessionId, currentTimeSeconds); + } else { + redisOperator.addToZSet(key, sessionId, currentTimeSeconds); + redisOperator.expire(key, QUEUE_TIMEOUT_SECONDS); + } } - - public Optional getRank(Long eventId, String sessionId) { - Long rank = redisOperator.getRankFromZSet(String.valueOf(eventId), sessionId); + + public Optional getRankInWaitingQueue(Long eventId, String sessionId) { + String key = generateWaitingQueueKey(eventId); + Long rank = redisOperator.getRankFromZSet(key, sessionId); return Optional.ofNullable(rank); } - public void remove(Long eventId, String sessionId) { - redisOperator.removeElementFromZSet(String.valueOf(eventId), sessionId); + public void removeFromWaitingQueue(Long eventId, String sessionId) { + String key = generateWaitingQueueKey(eventId); + redisOperator.removeElementFromZSet(key, sessionId); + } + + public void removeRangeByScoreFromWaitingQueue(Long eventId, double minScore, double maxScore) { + String key = generateWaitingQueueKey(eventId); + redisOperator.removeRangeByScoreFromZSet(key, minScore, maxScore); + } + + public void addToParticipantQueue(Long eventId, String sessionId, double currentTimeSeconds) { + String key = generateParticipantQueueKey(eventId); + + if(redisOperator.exists(key)) { + redisOperator.addToZSet(key, sessionId, currentTimeSeconds); + } else { + redisOperator.addToZSet(key, sessionId, currentTimeSeconds); + redisOperator.expire(key, QUEUE_TIMEOUT_SECONDS); + } + } + + public Long getSizeOfParticipantQueue(Long eventId) { + String key = generateParticipantQueueKey(eventId); + return redisOperator.getSizeOfZSet(key); + } + + public Double getElementScore(Long eventId, String sessionId) { + String key = generateParticipantQueueKey(eventId); + return redisOperator.getScoreFromZSet(key, sessionId); + } + + public void removeFromParticipantQueue(Long eventId, String sessionId) { + String key = generateParticipantQueueKey(eventId); + redisOperator.removeElementFromZSet(key, sessionId); + } + + public void removeRangeByScoreFromParticipantQueue(Long eventId, double minScore, double maxScore) { + String key = generateParticipantQueueKey(eventId); + redisOperator.removeRangeByScoreFromZSet(key, minScore, maxScore); + } + + private String generateWaitingQueueKey(Long eventId) { + return WAITING_QUEUE_KEY_PREFIX + eventId; } - public void removeRangeByScore(Long eventId, double minScore, double maxScore) { - redisOperator.removeRangeByScoreFromZSet(String.valueOf(eventId), minScore, maxScore); + private String generateParticipantQueueKey(Long eventId) { + return PARTICIPANT_QUEUE_KEY_PREFIX + eventId; } } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java index b89fd2a2..560d5c68 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java @@ -22,58 +22,82 @@ public class BookingQueueService { private final static double MILLISECONDS_PER_SECOND = 1000.0; - private final static double TIMEOUT_SECONDS = 7 * 60; - private final static long ENTRY_LIMIT = 2; + private final static long WAITING_QUEUE_TIMEOUT_SECONDS = 2 * 60 * 60; // 2 hours + private final static long PARTICIPANT_QUEUE_TIMEOUT_SECONDS = 7 * 60; // 7 minutes + private final static int ENTRY_LIMIT = 2; private final BookingQueueManager bookingQueueManager; private final BookingJwtProvider bookingJwtProvider; - public void enterQueue(BookingQueueEnterRequest request, String sessionId) { + public SessionIdIssueResponse issueSessionId() { + UUID sessionId = UUID.randomUUID(); + return SessionIdIssueResponse.from(sessionId.toString()); + } + + public void enterWaitingQueue(BookingQueueEnterRequest request, String sessionId) { double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND; - bookingQueueManager.add(request.eventId(), sessionId, currentTimeSeconds); + bookingQueueManager.addToWaitingQueue(request.eventId(), sessionId, currentTimeSeconds); } - public OrderInQueueGetResponse getOrderInQueue(Long eventId, String sessionId) { + public OrderInQueueGetResponse getWaitingOrder(Long eventId, String sessionId) { cleanQueue(eventId); - Long order = getOrder(eventId, sessionId); - Boolean isMyTurn = order <= ENTRY_LIMIT; - Long myOrder = isMyTurn ? 0 : order - ENTRY_LIMIT; + + Long myOrder = getOrderInWaitingQueue(eventId, sessionId); + Boolean isMyTurn = myOrder <= getAvailableEntryCountForParticipantQueue(eventId); + + if (isMyTurn) { + bookingQueueManager.removeFromWaitingQueue(eventId, sessionId); + double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND; + bookingQueueManager.addToParticipantQueue(eventId, sessionId, currentTimeSeconds); + } + return OrderInQueueGetResponse.of(myOrder, isMyTurn); } public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId) { - Long order = getOrder(request.eventId(), sessionId); - - if (order > ENTRY_LIMIT) { + if (!existsParticipant(request.eventId(), sessionId)) { throw new BookingException(BookingErrorCode.OUT_OF_ORDER); } BookingJwtPayload payload = new BookingJwtPayload(sessionId); String token = bookingJwtProvider.generateToken(payload); - return TokenIssueResponse.from(token); } - private Long getOrder(Long eventId, String sessionId) { - return bookingQueueManager.getRank(eventId, sessionId) + public void exitQueue(BookingQueueExitRequest request, String sessionId) { + bookingQueueManager.removeFromWaitingQueue(request.eventId(), sessionId); + bookingQueueManager.removeFromParticipantQueue(request.eventId(), sessionId); + } + + private Long getOrderInWaitingQueue(Long eventId, String sessionId) { + Long rank = bookingQueueManager.getRankInWaitingQueue(eventId, sessionId) .orElseThrow(() -> new BookingException(BookingErrorCode.NOT_IN_QUEUE)); + return rank + 1; } - public void exitQueue(BookingQueueExitRequest request, String sessionId) { - bookingQueueManager.remove(request.eventId(), sessionId); + private Long getAvailableEntryCountForParticipantQueue(Long eventId) { + Long participantCount = bookingQueueManager.getSizeOfParticipantQueue(eventId); + return ENTRY_LIMIT - participantCount; } - public SessionIdIssueResponse issueSessionId() { - UUID sessionId = UUID.randomUUID(); - return SessionIdIssueResponse.from(sessionId.toString()); + private Boolean existsParticipant(Long eventId, String sessionId) { + return bookingQueueManager.getElementScore(eventId, sessionId) != null; } /* - * 대기열에 존재하는 세션 중 타임아웃된 세션을 제거한다. + * 대기열, 참가열에 존재하는 세션 중 타임아웃된 세션을 제거한다. */ private void cleanQueue(Long eventId) { double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND; - double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS; - bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds); + bookingQueueManager.removeRangeByScoreFromWaitingQueue( + eventId, + 0, + currentTimeSeconds - WAITING_QUEUE_TIMEOUT_SECONDS + ); + bookingQueueManager.removeRangeByScoreFromParticipantQueue( + eventId, + 0, + currentTimeSeconds - PARTICIPANT_QUEUE_TIMEOUT_SECONDS + ); } } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java index b08d2dd9..558e81de 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java @@ -12,8 +12,8 @@ @RequiredArgsConstructor public class SeatLockManager { - private final static String SEAT_LOCK_CACHE_KEY_PREFIX = "seatId:"; - private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "sessionId:"; + private final static String SEAT_LOCK_KEY_PREFIX = "seatId:"; + private final static String SEAT_LOCK_VALUE_PREFIX = "sessionId:"; private final RedisOperator redisOperator; @@ -34,14 +34,14 @@ public void unlockSeat(Long seatId) { } private String generateSeatLockKey(Long seatId) { - return SEAT_LOCK_CACHE_KEY_PREFIX + seatId; + return SEAT_LOCK_KEY_PREFIX + seatId; } private String generateSeatLockValue(String tokenSessionId) { - return SEAT_LOCK_CACHE_VALUE_PREFIX + tokenSessionId; + return SEAT_LOCK_VALUE_PREFIX + tokenSessionId; } private String extractSessionId(String value) { - return value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, ""); + return value.replace(SEAT_LOCK_VALUE_PREFIX, ""); } } diff --git a/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java b/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java index 28799afb..38e163d8 100644 --- a/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java +++ b/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java @@ -162,7 +162,7 @@ void setup() { ); given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(SESSION_ID)); - doNothing().when(bookingQueueManager).remove(any(Long.class), any(String.class)); + doNothing().when(bookingQueueManager).removeFromWaitingQueue(any(Long.class), any(String.class)); // when BookingCreateResponse response = bookingService.createBooking(request, member.getId(), SESSION_ID); diff --git a/http/bingterpark.http b/http/bingterpark.http index 3810cef3..3c9abfdb 100644 --- a/http/bingterpark.http +++ b/http/bingterpark.http @@ -184,7 +184,7 @@ Booking-Session-Id: {{sessionId}} } ### 대기열 조회 -GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1 +GET http://localhost:8082/api/v1/bookings/waiting-order?eventId=1 Authorization: Bearer {{accessToken}} Booking-Session-Id: {{sessionId}}