-
Notifications
You must be signed in to change notification settings - Fork 0
Merge/redis concurrency into develop #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.catchtable.global.lock; | ||
|
|
||
| import com.catchtable.global.exception.CustomException; | ||
| import com.catchtable.global.exception.ErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.redisson.api.RLock; | ||
| import org.redisson.api.RedissonClient; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.function.Supplier; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class DistributedLockExecutor { | ||
|
|
||
| private final RedissonClient redissonClient; | ||
|
|
||
| /** | ||
| * λΆμ° λ½μ μ‘μ λ€ taskλ₯Ό μ€ννκ³ , λλλ©΄ λ½μ ν΄μ νλ€. | ||
| * | ||
| * @param lockKey λ½ ν€. κ°μ ν€λΌλ¦¬λ§ μ§λ ¬νλλ€. | ||
| * @param waitSeconds λ½μ μ‘μΌλ €κ³ κΈ°λ€λ¦¬λ μ΅λ μκ°(μ΄). | ||
| * @param leaseSeconds λ½μ μ‘μ λ€ μλμΌλ‘ ν리λ μκ°(μ΄). μμ μ΄ μ£½μ΄λ μ΄ μκ° ν ν΄μ λλ€. | ||
| * @param task λ½ μμμ μ€νν μμ . | ||
| */ | ||
| public <T> T executeWithLock(String lockKey, long waitSeconds, long leaseSeconds, Supplier<T> task) { | ||
| RLock lock = redissonClient.getLock(lockKey); | ||
| boolean acquired = false; | ||
| try { | ||
| acquired = lock.tryLock(waitSeconds, leaseSeconds, TimeUnit.SECONDS); | ||
| if (!acquired) { | ||
| log.warn("λΆμ° λ½ νλ μ€ν¨ (λκΈ° {}μ΄ μ΄κ³Ό): {}", waitSeconds, lockKey); | ||
| throw new CustomException(ErrorCode.LOCK_TIMEOUT); | ||
| } | ||
| return task.get(); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new CustomException(ErrorCode.LOCK_TIMEOUT); | ||
| } finally { | ||
| if (acquired && lock.isHeldByCurrentThread()) { | ||
| lock.unlock(); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| import com.catchtable.coupon.service.CouponService; | ||
| import com.catchtable.global.exception.CustomException; | ||
| import com.catchtable.global.exception.ErrorCode; | ||
| import com.catchtable.global.lock.DistributedLockExecutor; | ||
| import com.catchtable.notification.event.ReservationCanceledEvent; | ||
| import com.catchtable.notification.event.ReservationChangedEvent; | ||
| import com.catchtable.notification.event.ReservationVisitedEvent; | ||
|
|
@@ -39,7 +40,9 @@ | |
| import org.springframework.dao.OptimisticLockingFailureException; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.PlatformTransactionManager; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.transaction.support.TransactionTemplate; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalTime; | ||
|
|
@@ -65,6 +68,8 @@ public class ReservationService { | |
| private final PaymentRepository paymentRepository; | ||
| private final PaymentService paymentService; | ||
| private final StoreRepository storeRepository; | ||
| private final DistributedLockExecutor lockExecutor; | ||
| private final PlatformTransactionManager transactionManager; | ||
|
|
||
| // ============================================================ | ||
| // AI Tools | ||
|
|
@@ -85,7 +90,6 @@ public List<String> searchStoresByName( | |
| @Tool(description = "μ¬μ©μμ μμ°μ΄ μμ²μ κΈ°λ°μΌλ‘ λ μ€ν λ μμ½μ μμ±ν©λλ€. " + | ||
| "λ°λμ searchStoresByNameμΌλ‘ λ§€μ₯λͺ μ νμΈν ν νΈμΆνμΈμ. " + | ||
| "λ§€μ₯ μ΄λ¦, μμ½ λ μ§, μμ½ μκ°, μΈμμκ° λͺ¨λ νμν©λλ€.") | ||
| @Transactional | ||
| public String createReservationFromAi( | ||
| @ToolParam(description = "λ§€μ₯ μ΄λ¦ (μ νν μ΄λ¦ μ¬μ©)") String storeName, | ||
| @ToolParam(description = "μμ½ λ μ§, ISO νμ (μ: 2025-05-11)") LocalDate date, | ||
|
|
@@ -110,26 +114,34 @@ public String createReservationFromAi( | |
| return "STORE_OR_SLOT_NOT_FOUND: μμ²νμ λ§€μ₯(" + storeName + ")μ " + date + " " + time + " μκ°λμ μμ½ κ°λ₯ν μλ¦¬κ° μμ΅λλ€."; | ||
| } | ||
|
|
||
| Reservation saved = createReservationCore( | ||
| currentUserId, availableRemain.get().getId(), member, couponId); | ||
|
|
||
| // Payment λ μ½λ μμ± (κ²°μ μ°½ νΈμΆμ μν΄ orderId νμ) | ||
| String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); | ||
| Payment payment = Payment.builder() | ||
| .reservation(saved) | ||
| .orderId(orderId) | ||
| .amount(DEPOSIT_AMOUNT) | ||
| .build(); | ||
| paymentRepository.save(payment); | ||
|
|
||
| log.info("AI μμ½ μ±κ³΅: reservationId={}, orderId={}", saved.getId(), orderId); | ||
|
|
||
| PendingPaymentHolder.set(new PendingPaymentInfo(saved.getId(), orderId, DEPOSIT_AMOUNT)); | ||
|
|
||
| return String.format( | ||
| "λ€, %s λ μ€ν λ %s %s μκ°μΌλ‘ %dλͺ μμ½μ΄ μλ£λμμ΅λλ€. " + | ||
| "보μ¦κΈ 10,000μ κ²°μ ν μμ½μ΄ μ΅μ’ νμ λ©λλ€. μμ½ λ²νΈλ %dλ²μ λλ€.", | ||
| storeName, date, time, member, saved.getId()); | ||
| Long remainId = availableRemain.get().getId(); | ||
| String lockKey = "lock:reservation:remain:" + remainId; | ||
|
|
||
| return lockExecutor.executeWithLock(lockKey, 3, 5, () -> { | ||
| TransactionTemplate tx = new TransactionTemplate(transactionManager); | ||
| return tx.execute(status -> { | ||
| Reservation saved = createReservationCore( | ||
| currentUserId, remainId, member, couponId); | ||
|
|
||
| // Payment λ μ½λ μμ± (κ²°μ μ°½ νΈμΆμ μν΄ orderId νμ) | ||
| String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); | ||
| Payment payment = Payment.builder() | ||
| .reservation(saved) | ||
| .orderId(orderId) | ||
| .amount(DEPOSIT_AMOUNT) | ||
| .build(); | ||
| paymentRepository.save(payment); | ||
|
|
||
| log.info("AI μμ½ μ±κ³΅: reservationId={}, orderId={}", saved.getId(), orderId); | ||
|
|
||
| PendingPaymentHolder.set(new PendingPaymentInfo(saved.getId(), orderId, DEPOSIT_AMOUNT)); | ||
|
|
||
| return String.format( | ||
| "λ€, %s λ μ€ν λ %s %s μκ°μΌλ‘ %dλͺ μμ½μ΄ μλ£λμμ΅λλ€. " + | ||
| "보μ¦κΈ 10,000μ κ²°μ ν μμ½μ΄ μ΅μ’ νμ λ©λλ€. μμ½ λ²νΈλ %dλ²μ λλ€.", | ||
| storeName, date, time, member, saved.getId()); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| @Tool(description = "μ¬μ©μμ μμ½ λͺ©λ‘μ μ‘°νν©λλ€. 'λ΄ μμ½ λ³΄μ¬μ€', 'μμ½ νν©' λ±μ μμ²μ μ¬μ©νμΈμ.") | ||
|
|
@@ -200,20 +212,27 @@ public String cancelReservationFromAi( | |
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public ReservationCreateResponseDto create(Long userId, ReservationCreateRequestDto request) { | ||
| Reservation saved = createReservationCore(userId, request.remainId(), request.member(), request.couponId()); | ||
|
|
||
| // ConfirmedEventλ κ²°μ μλ£ μμ (PaymentService.confirmPayment)μμ λ°ννλ€. | ||
| String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); | ||
| Payment payment = Payment.builder() | ||
| .reservation(saved) | ||
| .orderId(orderId) | ||
| .amount(DEPOSIT_AMOUNT) | ||
| .build(); | ||
| paymentRepository.save(payment); | ||
|
|
||
| return new ReservationCreateResponseDto(saved.getId(), orderId, DEPOSIT_AMOUNT, saved.getStatus()); | ||
| String lockKey = "lock:reservation:remain:" + request.remainId(); | ||
|
|
||
| return lockExecutor.executeWithLock(lockKey, 3, 5, () -> { | ||
| TransactionTemplate tx = new TransactionTemplate(transactionManager); | ||
| return tx.execute(status -> { | ||
| Reservation saved = createReservationCore( | ||
| userId, request.remainId(), request.member(), request.couponId()); | ||
|
|
||
| // ConfirmedEventλ κ²°μ μλ£ μμ (PaymentService.confirmPayment)μμ λ°ννλ€. | ||
| String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); | ||
| Payment payment = Payment.builder() | ||
| .reservation(saved) | ||
| .orderId(orderId) | ||
| .amount(DEPOSIT_AMOUNT) | ||
| .build(); | ||
| paymentRepository.save(payment); | ||
|
|
||
| return new ReservationCreateResponseDto(saved.getId(), orderId, DEPOSIT_AMOUNT, saved.getStatus()); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -252,39 +271,45 @@ public void cancelReservation(Long reservationId, Long userId) { | |
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public ReservationUpdateResponseDto updateReservation(Long reservationId, Long userId, ReservationUpdateRequestDto request) { | ||
| Reservation oldReservation = cancelReservationCore(reservationId, userId, ReservationStatus.REPLACED); | ||
| StoreRemain oldStoreRemain = oldReservation.getStoreRemain(); | ||
|
|
||
| // κΈ°μ‘΄ κ²°μ λ₯Ό μ μμ½μΌλ‘ μ΄μ | ||
| Payment oldPayment = paymentRepository.findByReservation_Id(reservationId).orElse(null); | ||
|
|
||
| Reservation newReservation = createReservationCore(userId, request.newRemainId(), request.newMember(), request.couponId()); | ||
| newReservation.changeStatus(ReservationStatus.CONFIRMED); | ||
|
|
||
| if (oldPayment != null) { | ||
| oldPayment.transferToReservation(newReservation); | ||
| } | ||
|
|
||
| StoreRemain newStoreRemain = newReservation.getStoreRemain(); | ||
| eventPublisher.publishEvent(new ReservationChangedEvent( | ||
| newReservation.getId(), | ||
| userId, | ||
| newStoreRemain.getStore().getStoreName(), | ||
| oldStoreRemain.getRemainDate().toString(), | ||
| oldStoreRemain.getRemainTime().toString(), | ||
| newStoreRemain.getRemainDate().toString(), | ||
| newStoreRemain.getRemainTime().toString() | ||
| )); | ||
|
|
||
| return new ReservationUpdateResponseDto( | ||
| newReservation.getId(), | ||
| newReservation.getStoreRemain().getId(), | ||
| newReservation.getMember(), | ||
| newReservation.getStatus().name().toLowerCase(), | ||
| newReservation.getUpdatedAt() != null ? newReservation.getUpdatedAt() : java.time.LocalDateTime.now() | ||
| ); | ||
| String lockKey = "lock:reservation:remain:" + request.newRemainId(); | ||
|
|
||
| return lockExecutor.executeWithLock(lockKey, 3, 5, () -> { | ||
| TransactionTemplate tx = new TransactionTemplate(transactionManager); | ||
| return tx.execute(status -> { | ||
| Reservation oldReservation = cancelReservationCore(reservationId, userId, ReservationStatus.REPLACED); | ||
| StoreRemain oldStoreRemain = oldReservation.getStoreRemain(); | ||
|
|
||
| // κΈ°μ‘΄ κ²°μ λ₯Ό μ μμ½μΌλ‘ μ΄μ | ||
| Payment oldPayment = paymentRepository.findByReservation_Id(reservationId).orElse(null); | ||
|
|
||
| Reservation newReservation = createReservationCore(userId, request.newRemainId(), request.newMember(), request.couponId()); | ||
| newReservation.changeStatus(ReservationStatus.CONFIRMED); | ||
|
|
||
| if (oldPayment != null) { | ||
| oldPayment.transferToReservation(newReservation); | ||
| } | ||
|
|
||
| StoreRemain newStoreRemain = newReservation.getStoreRemain(); | ||
| eventPublisher.publishEvent(new ReservationChangedEvent( | ||
| newReservation.getId(), | ||
| userId, | ||
| newStoreRemain.getStore().getStoreName(), | ||
| oldStoreRemain.getRemainDate().toString(), | ||
| oldStoreRemain.getRemainTime().toString(), | ||
| newStoreRemain.getRemainDate().toString(), | ||
| newStoreRemain.getRemainTime().toString() | ||
| )); | ||
|
|
||
| return new ReservationUpdateResponseDto( | ||
| newReservation.getId(), | ||
| newReservation.getStoreRemain().getId(), | ||
| newReservation.getMember(), | ||
| newReservation.getStatus().name().toLowerCase(), | ||
| newReservation.getUpdatedAt() != null ? newReservation.getUpdatedAt() : java.time.LocalDateTime.now() | ||
| ); | ||
| }); | ||
| }); | ||
|
Comment on lines
274
to
+312
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. νμ¬ μμ½ λ³κ²½ λ‘μ§μ μλ‘μ΄ μμ½ μκ°λ( μ΄ κΈ°μ‘΄ μ΄ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄μλ κΈ°μ‘΄ |
||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
λΆμ° λ½μ λκΈ° μκ°(3μ΄)κ³Ό μλ μκ°(5μ΄)μ΄ νλμ½λ©λμ΄ μμ΅λλ€. μ΄ κ°λ€μ
create(218λΌμΈ),updateReservation(277λΌμΈ) λ©μλμμλ λ°λ³΅μ μΌλ‘ μ¬μ©λ©λλ€.μ½λμ μ μ§λ³΄μμ±κ³Ό μΌκ΄μ±μ λμ΄κΈ° μν΄ μ΄ κ°λ€μ ν΄λμ€ λ 벨μ
private static finalμμλ‘ μΆμΆνλ κ²μ κΆμ₯ν©λλ€.μμ:
μ΄λ κ² λ³κ²½νλ©΄ ν₯ν λ½ μ μ± λ³κ²½ μ ν κ³³μμλ§ μμ νλ©΄ λμ΄ κ΄λ¦¬κ° μ©μ΄ν΄μ§λλ€.