diff --git a/README.md b/README.md index 1d8d85b6..550d5fd1 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,21 @@ Reservation(예매)과 Seat(좌석)는 M:N 관계이므로 ReservedSeat 중간 ## 성능측정 -[성능측정보고서 링크](https://kind-artichoke-34f.notion.site/180a4bf15f1a805eadc6c3bbc20f74a9?pvs=4) \ No newline at end of file +[성능측정보고서 링크](https://kind-artichoke-34f.notion.site/180a4bf15f1a805eadc6c3bbc20f74a9?pvs=4) + +## 분산락 leaseTime 및 waitTime 설정 값과 이유 + +### 평균 소요 시간 +- 예매 API의 평균 소요 시간: 570ms + +### leaseTime 설정 값: 2초 +- 설정 이유 + - leaseTime은 트랜잭션 수행 중 락이 유지되는 시간입니다. + - 570ms는 평균 수행 시간이므로, **예외 상황(예: 네트워크 지연, 서버 부하 등)**을 고려하여 여유 시간을 추가해야 합니다. + - 일반적으로 평균 수행 시간의 2배 ~ 3배를 설정하는 것이 적당하므로, 2초로 설정했습니다. + +### waitTime 설정 값: 2초 +- 설정 이유: + - waitTime은 다른 요청이 락을 점유하고 있을 때, 현재 요청이 락 해제를 기다릴 최대 시간입니다. + - 평균 트랜잭션 수행 시간이 570ms이므로, 락이 해제될 때까지 기다릴 충분한 시간이라고 판단하여 2초로 설정했습니다. + - 이를 통해 대부분의 트랜잭션이 락 해제를 기다릴 여유를 가질 수 있습니다. diff --git a/application/src/main/java/com/example/aop/DistributedLock.java b/application/src/main/java/com/example/aop/DistributedLock.java index 69f46a5a..2ec6885c 100644 --- a/application/src/main/java/com/example/aop/DistributedLock.java +++ b/application/src/main/java/com/example/aop/DistributedLock.java @@ -10,7 +10,7 @@ @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { String key(); - long leaseTime() default 5; - long waitTime() default 5; + long leaseTime() default 2; + long waitTime() default 2; TimeUnit timeUnit() default TimeUnit.SECONDS; } diff --git a/application/src/main/java/com/example/func/DistributedLockExecutor.java b/application/src/main/java/com/example/func/DistributedLockExecutor.java new file mode 100644 index 00000000..7adff935 --- /dev/null +++ b/application/src/main/java/com/example/func/DistributedLockExecutor.java @@ -0,0 +1,45 @@ +package com.example.func; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Component +@RequiredArgsConstructor +public class DistributedLockExecutor { + + private final RedissonClient redissonClient; + + public T executeWithLock(List lockKeys, long waitTime, long leaseTime, TimeUnit timeUnit, Supplier action) { + List locks = lockKeys.stream() + .map(redissonClient::getLock) + .toList(); + boolean allLocked = false; + + try { + allLocked = redissonClient.getMultiLock(locks.toArray(new RLock[0])) + .tryLock(waitTime, leaseTime, timeUnit); + + if (!allLocked) { + throw new IllegalArgumentException("해당 좌석은 현재 다른 사용자가 예매를 진행하고 있습니다."); + } + + return action.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (allLocked) { + locks.forEach(lock -> { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + }); + } + } + } +} diff --git a/application/src/main/java/com/example/reservation/ReservationService.java b/application/src/main/java/com/example/reservation/ReservationService.java index 7dd89683..08e1391f 100644 --- a/application/src/main/java/com/example/reservation/ReservationService.java +++ b/application/src/main/java/com/example/reservation/ReservationService.java @@ -1,8 +1,8 @@ package com.example.reservation; -import com.example.aop.DistributedLock; import com.example.entity.reservation.Reservation; import com.example.entity.reservation.ReservedSeat; +import com.example.func.DistributedLockExecutor; import com.example.message.MessageService; import com.example.repository.reservation.ReservationRepository; import com.example.reservation.request.ReservationServiceRequest; @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -22,20 +23,27 @@ public class ReservationService { private final MessageService messageService; private final ReservationValidate reservationValidate; private final ReservationRepository reservationRepository; + private final DistributedLockExecutor lockExecutor; +// @DistributedLock(key = "reservation:screening:{#request.screeningId}:seat:{#request.seatIds}", leaseTime = 2, waitTime = 2) @Transactional - @DistributedLock(key = "reservation:screening:{#request.screeningId}:seat:{#request.seatIds}", leaseTime = 10, waitTime = 5) public ReservationServiceResponse reserve(ReservationServiceRequest request) { - ReservationValidationResult validationResult = reservationValidate.validate(request); + List lockKeys = request.getSeatIds().stream() + .map(seatId -> "reservation:screening:" + request.getScreeningId() + ":seat:" + seatId) + .toList(); + + return lockExecutor.executeWithLock(lockKeys, 2, 2, TimeUnit.SECONDS, () -> { + ReservationValidationResult validationResult = reservationValidate.validate(request); - Reservation reservation = createReservation(validationResult); + Reservation reservation = createReservation(validationResult); - reservationRepository.save(reservation); + reservationRepository.save(reservation); - messageService.send(); + messageService.send(); - return ReservationServiceResponse.of(reservation); + return ReservationServiceResponse.of(reservation); + }); } private Reservation createReservation(ReservationValidationResult validationResult) { diff --git a/application/src/main/java/com/example/reservation/validator/ReservationValidate.java b/application/src/main/java/com/example/reservation/validator/ReservationValidate.java index 55fbde74..a4acd4bc 100644 --- a/application/src/main/java/com/example/reservation/validator/ReservationValidate.java +++ b/application/src/main/java/com/example/reservation/validator/ReservationValidate.java @@ -10,6 +10,7 @@ import com.example.repository.reservation.ReservedSeatRepository; import com.example.reservation.request.ReservationServiceRequest; import lombok.RequiredArgsConstructor; +import org.redisson.api.RScript; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; @@ -84,33 +85,30 @@ private Seats getSeats(List seatsIds) { return new Seats(seatRepository.findAllById(seatsIds)); } -// private void validateAndReserveSeatsInRedis(Long screeningId, List seatIds) { -// String redisKey = "screening:" + screeningId + ":seats"; -// -// // Lua 스크립트: 좌석 상태를 확인하고, 예약되지 않은 경우 예약 상태로 변경 -// String luaScript = -// "for i, seatId in ipairs(ARGV) do " + -// " if redis.call('HGET', KEYS[1], seatId) == 'true' then " + // 이미 예약된 좌석 확인 -// " return 0; " + -// " end; " + -// "end; " + -// "for i, seatId in ipairs(ARGV) do " + -// " redis.call('HSET', KEYS[1], seatId, 'true'); " + // 좌석 상태를 예약으로 변경 -// "end; " + -// "return 1;"; -// -// // Redisson의 Lua 실행 API 호출 -// Long result = redissonClient.getScript().eval( -// RScript.Mode.READ_WRITE, -// luaScript, -// RScript.ReturnType.INTEGER, -// List.of(redisKey), // Redis 키 -// seatIds.stream().map(String::valueOf).toArray() // ARGV: 좌석 ID 리스트 -// ); -// -// // Lua 스크립트 결과가 0이면 좌석 예약 실패 처리 -// if (result == 0) { -// throw new IllegalArgumentException("이미 예매된 좌석입니다."); -// } -// } + private void validateAndReserveSeatsInRedis(Long screeningId, List seatIds) { + String redisKey = "screening:" + screeningId + ":seats"; + + String luaScript = + "for i, seatId in ipairs(ARGV) do " + + " if redis.call('HGET', KEYS[1], seatId) == 'true' then " + + " return 0; " + + " end; " + + "end; " + + "for i, seatId in ipairs(ARGV) do " + + " redis.call('HSET', KEYS[1], seatId, 'true'); " + + "end; " + + "return 1;"; + + Long result = redissonClient.getScript().eval( + RScript.Mode.READ_WRITE, + luaScript, + RScript.ReturnType.INTEGER, + List.of(redisKey), + seatIds.stream().map(String::valueOf).toArray() + ); + + if (result == 0) { + throw RESERVATION_EXIST_ERROR.exception(); + } + } }