diff --git a/services/order/src/main/java/com/ticketPing/order/presentation/request/CreateOrderRequest.java b/services/order/src/main/java/com/ticketPing/order/presentation/request/CreateOrderRequest.java new file mode 100644 index 00000000..e34825ae --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/presentation/request/CreateOrderRequest.java @@ -0,0 +1,9 @@ +package com.ticketPing.order.presentation.request; + +import java.util.UUID; + +public record CreateOrderRequest ( + UUID scheduleId, + UUID seatId +) { +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/dtos/SeatResponse.java b/services/performance/src/main/java/com/ticketPing/performance/application/dtos/SeatResponse.java index 65e111f3..7f67f6e3 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/dtos/SeatResponse.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/dtos/SeatResponse.java @@ -14,8 +14,7 @@ public record SeatResponse ( Integer row, Integer col, String seatStatus, - String seatGrade, - Integer cost + String seatGrade ) { public static SeatResponse of(Seat seat) { return SeatResponse.builder() @@ -24,7 +23,6 @@ public static SeatResponse of(Seat seat) { .col(seat.getCol()) .seatStatus(seat.getSeatStatus().getValue()) .seatGrade(seat.getSeatCost().getSeatGrade()) - .cost(seat.getSeatCost().getCost()) .build(); } @@ -35,7 +33,6 @@ public static SeatResponse of(SeatCache seatCache) { .col(seatCache.getCol()) .seatStatus(seatCache.getSeatStatus()) .seatGrade(seatCache.getSeatGrade()) - .cost(seatCache.getCost()) .build(); } } diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java b/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java index 18f73255..345ef60a 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java @@ -1,49 +1,49 @@ package com.ticketPing.performance.application.scheduler; +import com.ticketPing.performance.application.service.NotificationService; import com.ticketPing.performance.application.service.PerformanceService; import com.ticketPing.performance.domain.model.entity.Performance; -import com.ticketPing.performance.infrastructure.service.DiscordNotificationService; +import com.ticketPing.performance.infrastructure.service.DistributedLockService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -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; - @Slf4j @Component @RequiredArgsConstructor public class SeatCacheScheduler { private final PerformanceService performanceService; - private final RedissonClient redissonClient; - private final DiscordNotificationService discordNotificationService; + private final DistributedLockService lockService; + private final NotificationService notificationService; private static final String LOCK_KEY = "SchedulerLock"; private static final int LOCK_TIMEOUT = 300; @Scheduled(cron = "0 0/10 * * * *") public void runScheduler() { + log.info("Scheduler triggered"); try { - log.info("Scheduler started"); - RLock lock = redissonClient.getLock(LOCK_KEY); - boolean acquired = lock.tryLock(0, LOCK_TIMEOUT, TimeUnit.SECONDS); - - if (acquired) { - Performance performance = performanceService.getUpcomingPerformance(); - if (performance != null) { - performanceService.cacheAllSeatsForPerformance(performance.getId()); - log.info("Caching completed"); - } else { - log.info("No upcoming performance"); - } - } else { + boolean executed = lockService.executeWithLock(LOCK_KEY, 0, LOCK_TIMEOUT, this::cacheSeatsForUpcomingPerformance); + if (!executed) { log.warn("Another server is running the scheduler"); } } catch (Exception e) { - log.error("Error occurred during execution: {}", e.getMessage(), e); - discordNotificationService.sendErrorNotification(e.getMessage()); + log.error("Unexpected error in scheduler: {}", e.getMessage(), e); + notificationService.sendErrorNotification( + String.format("Error in SeatCacheScheduler: %s", e.getMessage()) + ); + } + } + + private void cacheSeatsForUpcomingPerformance() { + Performance performance = performanceService.getUpcomingPerformance(); + if (performance != null) { + performanceService.cacheAllSeatsForPerformance(performance.getId()); + log.info("Caching completed for performance ID: {}", performance.getId()); + } else { + log.info("No upcoming performance found"); } } } + diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/NotificationService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/NotificationService.java new file mode 100644 index 00000000..ec589818 --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/NotificationService.java @@ -0,0 +1,5 @@ +package com.ticketPing.performance.application.service; + +public interface NotificationService { + void sendErrorNotification(String errorMessage); +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java index 51b44381..441c6aee 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java @@ -7,10 +7,11 @@ import com.ticketPing.performance.domain.model.entity.Performance; import com.ticketPing.performance.domain.model.entity.Schedule; import com.ticketPing.performance.domain.repository.PerformanceRepository; +import com.ticketPing.performance.infrastructure.repository.CacheRepositoryImpl; import exception.ApplicationException; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -23,6 +24,7 @@ public class PerformanceService { private final PerformanceRepository performanceRepository; + private final CacheRepositoryImpl cacheRepositoryImpl; private final SeatService seatService; public PerformanceResponse getPerformance(UUID performanceId) { @@ -30,7 +32,7 @@ public PerformanceResponse getPerformance(UUID performanceId) { return PerformanceResponse.of(performance); } - public Page getAllPerformances(Pageable pageable) { + public Slice getAllPerformances(Pageable pageable) { return performanceRepository.findAll(pageable) .map(PerformanceListResponse::of); } @@ -58,8 +60,7 @@ public void cacheAllSeatsForPerformance(UUID performanceId) { long availableSeats = seatService.cacheSeatsForSchedule(schedule); totalAvailableSeats += availableSeats; } - - seatService.cacheAvailableSeatsForPerformance(performanceId, totalAvailableSeats); + cacheRepositoryImpl.cacheAvailableSeats(performanceId, totalAvailableSeats); } private Performance findPerformanceWithSchedules(UUID id) { diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/ScheduleService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/ScheduleService.java index 7e0befe5..92ae35b9 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/service/ScheduleService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/ScheduleService.java @@ -1,13 +1,8 @@ package com.ticketPing.performance.application.service; -import com.ticketPing.performance.application.dtos.ScheduleResponse; import com.ticketPing.performance.application.dtos.SeatResponse; -import com.ticketPing.performance.common.exception.ScheduleExceptionCase; -import com.ticketPing.performance.domain.model.entity.Schedule; import com.ticketPing.performance.domain.model.entity.SeatCache; -import com.ticketPing.performance.domain.repository.ScheduleRepository; -import com.ticketPing.performance.infrastructure.service.CacheService; -import exception.ApplicationException; +import com.ticketPing.performance.infrastructure.repository.CacheRepositoryImpl; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,21 +14,10 @@ @RequiredArgsConstructor public class ScheduleService { - private final ScheduleRepository scheduleRepository; - private final CacheService cacheService; - - public ScheduleResponse getSchedule(UUID id) { - Schedule schedule = findScheduleById(id); - return ScheduleResponse.of(schedule); - } + private final CacheRepositoryImpl cacheRepositoryImpl; public List getAllScheduleSeats(UUID scheduleId) { - Map seatMap = cacheService.getSeatsFromCache(scheduleId); + Map seatMap = cacheRepositoryImpl.getSeatCaches(scheduleId); return seatMap.values().stream().map(SeatResponse::of).toList(); } - - private Schedule findScheduleById(UUID id) { - return scheduleRepository.findById(id) - .orElseThrow(() -> new ApplicationException(ScheduleExceptionCase.SCHEDULE_NOT_FOUND)); - } } diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java index f24f0113..6544ee03 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java @@ -1,102 +1,105 @@ package com.ticketPing.performance.application.service; import com.ticketPing.performance.application.dtos.OrderSeatResponse; -import com.ticketPing.performance.application.dtos.SeatResponse; import com.ticketPing.performance.common.exception.SeatExceptionCase; import com.ticketPing.performance.domain.model.entity.Schedule; import com.ticketPing.performance.domain.model.entity.Seat; import com.ticketPing.performance.domain.model.entity.SeatCache; import com.ticketPing.performance.domain.model.enums.SeatStatus; +import com.ticketPing.performance.domain.repository.CacheRepository; import com.ticketPing.performance.domain.repository.SeatRepository; -import com.ticketPing.performance.infrastructure.service.CacheService; import com.ticketPing.performance.infrastructure.service.LuaScriptService; import exception.ApplicationException; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import static com.ticketPing.performance.common.constants.SeatConstants.PRE_RESERVE_TTL; + @Service @RequiredArgsConstructor public class SeatService { private final SeatRepository seatRepository; - private final CacheService cacheService; - private final LuaScriptService luaScriptService; - - @Value("${seat.pre-reserve-ttl}") - private int PRE_RESERVE_TTL; - - public SeatResponse getSeat(UUID id) { - Seat seat = seatRepository.findByIdWithSeatCost(id) - .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); - return SeatResponse.of(seat); - } + private final CacheRepository cacheRepository; public void preReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { - luaScriptService.preReserveSeat(scheduleId, seatId, userId); + cacheRepository.preReserveSeatCache(scheduleId, seatId, userId); } public void cancelPreReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { validatePreserve(scheduleId, seatId, userId); - cacheService.canclePreReserveSeat(scheduleId, seatId); + cancelPreReserveSeatInCache(scheduleId, seatId); + } + + @Transactional + public void reserveSeat(String scheduleId, String seatId) { + reserveSeatInDB(seatId); + reserveSeatInCache(UUID.fromString(scheduleId), UUID.fromString(seatId)); } public OrderSeatResponse getOrderSeatInfo(UUID scheduleId, UUID seatId, UUID userId) { validatePreserve(scheduleId, seatId, userId); - - Seat seat = seatRepository.findByIdWithAll(seatId) - .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); - + Seat seat = getSeatWithDetails(seatId); + extendPreReserveTTL(scheduleId, seatId); return OrderSeatResponse.of(seat); } public void extendPreReserveTTL(UUID scheduleId, UUID seatId) { - cacheService.extendPreReserveTTL(scheduleId, seatId, Duration.ofSeconds(PRE_RESERVE_TTL)); - } - - public void reserveSeat(String scheduleId, String seatId) { - Seat seat = seatRepository.findById(UUID.fromString(seatId)) - .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); - cacheService.reserveSeat(scheduleId, seatId); + cacheRepository.extendPreReserveTTL(scheduleId, seatId, Duration.ofSeconds(PRE_RESERVE_TTL)); } public long cacheSeatsForSchedule(Schedule schedule) { List seats = seatRepository.findByScheduleWithSeatCost(schedule); - Map seatMap = seats.stream() - .collect(Collectors.toMap(seat -> seat.getId().toString(), SeatCache::from)); + Map seatMap = seats.stream().collect(Collectors.toMap(seat -> seat.getId().toString(), SeatCache::from)); LocalDateTime expiration = schedule.getStartDate().atTime(23, 59, 59); Duration ttl = Duration.between(LocalDateTime.now(), expiration); - cacheService.cacheSeats(schedule.getId(), seatMap, ttl); + cacheRepository.cacheSeats(schedule.getId(), seatMap, ttl); - return seats.stream() - .filter(seat -> seat.getSeatStatus() == SeatStatus.AVAILABLE) - .count(); + return seats.stream().filter(seat -> seat.getSeatStatus() == SeatStatus.AVAILABLE).count(); } - public void cacheAvailableSeatsForPerformance(UUID performanceId, long availableSeats) { - cacheService.cacheAvailableSeats(performanceId, availableSeats); + private Seat getSeatWithDetails(UUID seatId) { + return seatRepository.findByIdWithAll(seatId) + .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); } - private void validatePreserve(UUID scheduleId, UUID seatId, UUID userId) { - SeatCache seatCache = cacheService.getSeatFromCache(scheduleId, seatId); + @Transactional + private void reserveSeatInDB(String seatId) { + Seat seat = seatRepository.findById(UUID.fromString(seatId)) + .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); + seat.reserveSeat(); + } - if(!seatCache.getSeatStatus().equals(SeatStatus.HELD.getValue())) { - throw new ApplicationException(SeatExceptionCase.SEAT_NOT_PRE_RESERVED); - } else if(!seatCache.getUserId().equals(userId)) { - System.out.println(seatCache.getUserId() + " " + userId); + private void validatePreserve(UUID scheduleId, UUID seatId, UUID userId) { + String preReserveUserId = cacheRepository.getPreReservTTL(scheduleId, seatId); + if(!preReserveUserId.equals(userId.toString())) throw new ApplicationException(SeatExceptionCase.USER_NOT_MATCH); - } + } + + private void cancelPreReserveSeatInCache(UUID scheduleId, UUID seatId) { + cacheRepository.deletePreReserveTTL(scheduleId, seatId); + SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId); + seatCache.cancelPreReserveSeat(); + cacheRepository.putSeatCache(seatCache, scheduleId, seatId); + } + + private void reserveSeatInCache(UUID scheduleId, UUID seatId) { + cacheRepository.deletePreReserveTTL(scheduleId, seatId); + SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId); + seatCache.reserveSeat(); + cacheRepository.putSeatCache(seatCache, scheduleId, seatId); } } + diff --git a/services/performance/src/main/java/com/ticketPing/performance/domain/model/entity/SeatCache.java b/services/performance/src/main/java/com/ticketPing/performance/domain/model/entity/SeatCache.java index 0c969612..348d6a6c 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/domain/model/entity/SeatCache.java +++ b/services/performance/src/main/java/com/ticketPing/performance/domain/model/entity/SeatCache.java @@ -17,8 +17,6 @@ public class SeatCache { private Integer col; private String seatStatus; private String seatGrade; - private Integer cost; - private UUID userId; public static SeatCache from(Seat seat) { return SeatCache.builder() @@ -27,13 +25,11 @@ public static SeatCache from(Seat seat) { .col(seat.getCol()) .seatStatus(seat.getSeatStatus().getValue()) .seatGrade(seat.getSeatCost().getSeatGrade()) - .cost(seat.getSeatCost().getCost()) .build(); } public void cancelPreReserveSeat() { seatStatus = SeatStatus.AVAILABLE.getValue(); - userId = null; } public void reserveSeat() { diff --git a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/CacheRepository.java b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/CacheRepository.java new file mode 100644 index 00000000..bff5a37c --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/CacheRepository.java @@ -0,0 +1,27 @@ +package com.ticketPing.performance.domain.repository; + +import com.ticketPing.performance.domain.model.entity.SeatCache; + +import java.time.Duration; +import java.util.Map; +import java.util.UUID; + +public interface CacheRepository { + void cacheSeats(UUID scheduleId, Map seatMap, Duration ttl); + + Map getSeatCaches(UUID scheduleId); + + SeatCache getSeatCache(UUID scheduleId, UUID seatId); + + void putSeatCache(SeatCache seatCache, UUID scheduleId, UUID seatId); + + void preReserveSeatCache(UUID scheduleId, UUID seatId, UUID userId); + + String getPreReservTTL(UUID scheduleId, UUID seatId); + + void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl); + + void deletePreReserveTTL(UUID scheduleId, UUID seatId); + + void cacheAvailableSeats(UUID performanceId, long availableSeats); +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java index 0a33bcf0..f130ed62 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java @@ -1,8 +1,8 @@ package com.ticketPing.performance.domain.repository; import com.ticketPing.performance.domain.model.entity.Performance; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import java.time.LocalDateTime; import java.util.Optional; @@ -11,7 +11,7 @@ public interface PerformanceRepository { Performance save(Performance performance); - Page findAll(Pageable pageable); + Slice findAll(Pageable pageable); Performance findByName(String name); diff --git a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/SeatRepository.java b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/SeatRepository.java index 87f8e0b0..3144ceb9 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/SeatRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/SeatRepository.java @@ -14,7 +14,5 @@ public interface SeatRepository { Optional findByIdWithAll(UUID seatId); - Optional findByIdWithSeatCost(UUID id); - List findByScheduleWithSeatCost(Schedule schedule); } diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java similarity index 60% rename from services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java rename to services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java index b0ea0f2d..6ba9b68b 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java @@ -1,7 +1,9 @@ -package com.ticketPing.performance.infrastructure.service; +package com.ticketPing.performance.infrastructure.repository; import com.ticketPing.performance.common.exception.SeatExceptionCase; import com.ticketPing.performance.domain.model.entity.SeatCache; +import com.ticketPing.performance.domain.repository.CacheRepository; +import com.ticketPing.performance.infrastructure.service.LuaScriptService; import exception.ApplicationException; import lombok.RequiredArgsConstructor; import org.redisson.api.RBucket; @@ -19,67 +21,66 @@ @Service @RequiredArgsConstructor -public class CacheService { +public class CacheRepositoryImpl implements CacheRepository { private final RedissonClient redissonClient; + private final LuaScriptService luaScriptService; public void cacheSeats(UUID scheduleId, Map seatMap, Duration ttl) { String key = "seat:{" + scheduleId + "}"; - RMap seatCache = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); seatCache.putAll(seatMap); seatCache.expire(ttl); } - public Map getSeatsFromCache(UUID scheduleId) { + public Map getSeatCaches(UUID scheduleId) { String key = "seat:{" + scheduleId + "}"; RMap seatCacheRMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); return seatCacheRMap.readAllMap(); } - public SeatCache getSeatFromCache(UUID scheduleId, UUID seatId) { - String key = "seat:{" + scheduleId + "}"; - RMap seatCacheMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); - + public SeatCache getSeatCache(UUID scheduleId, UUID seatId) { + String seatKey = "seat:{" + scheduleId + "}"; + RMap seatCacheMap = redissonClient.getMap(seatKey, JsonJacksonCodec.INSTANCE); return Optional.ofNullable(seatCacheMap.get(seatId.toString())) .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_CACHE_NOT_FOUND)); } - public void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl) { - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; - RBucket bucket = redissonClient.getBucket(ttlKey); - if (!bucket.isExists()) { - throw new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST); - } - bucket.expire(ttl); + public void putSeatCache(SeatCache seatCache, UUID scheduleId, UUID seatId) { + String seatKey = "seat:{" + scheduleId + "}"; + RMap seatCacheMap = redissonClient.getMap(seatKey, JsonJacksonCodec.INSTANCE); + seatCacheMap.put(seatId.toString(), seatCache); } - public void canclePreReserveSeat(UUID scheduleId, UUID seatId) { - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; - redissonClient.getBucket(ttlKey).delete(); - - String key = "seat:{" + scheduleId + "}"; - RMap seatCacheMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); - - SeatCache seatCache = Optional.ofNullable(seatCacheMap.get(seatId.toString())) - .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_CACHE_NOT_FOUND)); - seatCache.cancelPreReserveSeat(); + public void preReserveSeatCache(UUID scheduleId, UUID seatId, UUID userId) { + luaScriptService.preReserveSeat(scheduleId, seatId, userId); + } - seatCacheMap.put(seatId.toString(), seatCache); + public String getPreReservTTL(UUID scheduleId, UUID seatId) { + String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + RBucket bucket = redissonClient.getBucket(ttlKey); + return Optional.ofNullable(bucket.get()) + .orElseThrow(() -> new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST)); } - public void reserveSeat(String scheduleId, String seatId) { + public void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl) { String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; - redissonClient.getBucket(ttlKey).delete(); + RBucket bucket = redissonClient.getBucket(ttlKey); - String key = "seat:{" + scheduleId + "}"; - RMap seatCacheMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); + boolean success = bucket.expire(ttl); + if (!success) { + throw new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST); + } + } - SeatCache seatCache = Optional.ofNullable(seatCacheMap.get(seatId.toString())) - .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_CACHE_NOT_FOUND)); - seatCache.reserveSeat(); + public void deletePreReserveTTL(UUID scheduleId, UUID seatId) { + String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + RBucket bucket = redissonClient.getBucket(ttlKey); - seatCacheMap.put(seatId.toString(), seatCache); + boolean deleted = bucket.delete(); + if (!deleted) { + throw new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST); + } } public void cacheAvailableSeats(UUID performanceId, long availableSeats) { diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java index c6591f67..dd95bf7e 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java @@ -11,10 +11,6 @@ import java.util.UUID; public interface SeatJpaRepository extends SeatRepository, JpaRepository { - @Query(value = "select s from Seat s " + - "join fetch s.seatCost sc " + - "where s.id=:seatId") - Optional findByIdWithSeatCost(UUID seatId); @Query(value = "select s from Seat s " + "join fetch s.seatCost sc " + diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DiscordNotificationService.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DiscordNotificationService.java index a56cdcd4..177eae3f 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DiscordNotificationService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DiscordNotificationService.java @@ -1,5 +1,6 @@ package com.ticketPing.performance.infrastructure.service; +import com.ticketPing.performance.application.service.NotificationService; import com.ticketPing.performance.common.exception.MessageExceptionCase; import exception.ApplicationException; import lombok.RequiredArgsConstructor; @@ -15,13 +16,14 @@ @Slf4j @Service @RequiredArgsConstructor -public class DiscordNotificationService { +public class DiscordNotificationService implements NotificationService { @Value("${discord.webhook-url}") private String discordWebhookUrl; private final RestTemplate restTemplate; + @Override public void sendErrorNotification(String errorMessage) { try { String message = String.format("**Error occurred in SeatCacheScheduler**: %s", errorMessage); diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DistributedLockService.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DistributedLockService.java new file mode 100644 index 00000000..146d92d8 --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/DistributedLockService.java @@ -0,0 +1,34 @@ +package com.ticketPing.performance.infrastructure.service; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class DistributedLockService { + private final RedissonClient redissonClient; + + public boolean executeWithLock(String lockKey, int waitTimeout, int lockTimeout, Runnable task) { + RLock lock = redissonClient.getLock(lockKey); + try { + boolean acquired = lock.tryLock(waitTimeout, lockTimeout, TimeUnit.SECONDS); + if (acquired) { + try { + task.run(); + return true; + } finally { + lock.unlock(); + } + } else { + return false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Failed to acquire lock", e); + } + } +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/PerformanceController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/PerformanceController.java index a090356b..734dfbe8 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/PerformanceController.java +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/PerformanceController.java @@ -6,8 +6,8 @@ import com.ticketPing.performance.application.service.PerformanceService; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import response.CommonResponse; @@ -32,8 +32,8 @@ public ResponseEntity> getPerformance(@PathV @Operation(summary = "공연 목록 조회") @GetMapping - public ResponseEntity>> getAllPerformances(Pageable pageable) { - Page response = performanceService.getAllPerformances(pageable); + public ResponseEntity>> getAllPerformances(Pageable pageable) { + Slice response = performanceService.getAllPerformances(pageable); return ResponseEntity .status(200) .body(CommonResponse.success(response)); diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/ScheduleController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/ScheduleController.java index 565df2c4..71b03d1e 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/ScheduleController.java +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/ScheduleController.java @@ -1,6 +1,5 @@ package com.ticketPing.performance.presentation.controller; -import com.ticketPing.performance.application.dtos.ScheduleResponse; import com.ticketPing.performance.application.dtos.SeatResponse; import com.ticketPing.performance.application.service.ScheduleService; import io.swagger.v3.oas.annotations.Operation; @@ -20,14 +19,6 @@ @RequiredArgsConstructor public class ScheduleController { private final ScheduleService scheduleService; - @Operation(summary = "스케줄 조회") - @GetMapping("/{scheduleId}") - public ResponseEntity> getSchedule(@PathVariable("scheduleId") UUID scheduleId) { - ScheduleResponse scheduleResponse = scheduleService.getSchedule(scheduleId); - return ResponseEntity - .status(200) - .body(CommonResponse.success(scheduleResponse)); - } @Operation(summary = "스케줄 전체 좌석 조회") @GetMapping("/{scheduleId}/seats") diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java index e1ae2c0a..bf2e703a 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java @@ -20,18 +20,18 @@ public class SeatClientController { @Operation(summary = "좌석 주문 정보 조회 (order 서비스에서 호출용)") @GetMapping("/{seatId}/order-info") public ResponseEntity> getOrderSeatInfo(@RequestHeader("X_USER_ID") UUID userId, - @RequestParam("scheduleId") UUID scheduleId, - @PathVariable("seatId") UUID seatId) { + @PathVariable("seatId") UUID seatId, + @RequestParam("scheduleId") UUID scheduleId) { OrderSeatResponse orderSeatResponse = seatService.getOrderSeatInfo(scheduleId, seatId, userId); return ResponseEntity .status(200) .body(CommonResponse.success(orderSeatResponse)); } - @Operation(summary = "좌석 선점 ttl 연강 (order 서비스 호출용)") + @Operation(summary = "좌석 선점 ttl 연장 (order 서비스 호출용)") @PostMapping("/{seatId}/extend-ttl") - ResponseEntity> extendPreReserveTTL (@RequestParam("scheduleId") UUID scheduleId, - @PathVariable("seatId") UUID seatId) { + ResponseEntity> extendPreReserveTTL (@PathVariable("seatId") UUID seatId, + @RequestParam("scheduleId") UUID scheduleId) { seatService.extendPreReserveTTL(scheduleId, seatId); return ResponseEntity .status(200) diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java index 2c6b695f..0924b36e 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java @@ -1,7 +1,5 @@ package com.ticketPing.performance.presentation.controller; -import com.ticketPing.performance.application.dtos.OrderSeatResponse; -import com.ticketPing.performance.application.dtos.SeatResponse; import com.ticketPing.performance.application.service.SeatService; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -17,21 +15,12 @@ public class SeatController { private final SeatService seatService; - @Operation(summary = "좌석 정보 조회") - @GetMapping("/{seatId}") - public ResponseEntity> getSeat(@PathVariable("seatId") UUID seatId) { - SeatResponse seatResponse = seatService.getSeat(seatId); - return ResponseEntity - .status(200) - .body(CommonResponse.success(seatResponse)); - } - @Operation(summary = "좌석 선점") @PostMapping("/{seatId}/pre-reserve") public ResponseEntity> preReserveSeat(@RequestHeader("X_USER_ID") UUID userId, + @PathVariable("seatId") UUID seatId, @RequestParam("performanceId") UUID performanceId, - @RequestParam("scheduleId") UUID scheduleId, - @PathVariable("seatId") UUID seatId) { + @RequestParam("scheduleId") UUID scheduleId) { seatService.preReserveSeat(scheduleId, seatId, userId); return ResponseEntity .status(200) @@ -41,9 +30,9 @@ public ResponseEntity> preReserveSeat(@RequestHeader("X_U @Operation(summary = "좌석 선점 취소") @PostMapping("/{seatId}/cancel-reserve") public ResponseEntity> cancelPreReserveSeat(@RequestHeader("X_USER_ID") UUID userId, + @PathVariable("seatId") UUID seatId, @RequestParam("performanceId") UUID performanceId, - @RequestParam("scheduleId") UUID scheduleId, - @PathVariable("seatId") UUID seatId) { + @RequestParam("scheduleId") UUID scheduleId) { seatService.cancelPreReserveSeat(scheduleId, seatId, userId); return ResponseEntity .status(200) diff --git a/services/performance/src/main/resources/scripts/preReserveScript.lua b/services/performance/src/main/resources/scripts/preReserveScript.lua index 759b0089..c685e6ae 100644 --- a/services/performance/src/main/resources/scripts/preReserveScript.lua +++ b/services/performance/src/main/resources/scripts/preReserveScript.lua @@ -18,10 +18,9 @@ if seatObj.seatStatus ~= "AVAILABLE" then end seatObj.seatStatus = "HELD" -seatObj.userId = userId redis.call("HSET", hashKey, seatId, cjson.encode(seatObj)) -redis.call("SET", ttlKey, "HELD") +redis.call("SET", ttlKey, userId) redis.call("EXPIRE", ttlKey, ttl) return "SUCCESS" \ No newline at end of file