Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-aop"

//jwt
implementation 'io.jsonwebtoken:jjwt:0.12.6'
Expand Down Expand Up @@ -58,6 +59,10 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.testcontainers:testcontainers:1.21.0'
testImplementation 'org.testcontainers:junit-jupiter:1.21.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package nbc.ticketing.ticket911.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LettuceMultiLock {
String key();

String group() default "";

long waitTime() default 10L;

long leaseTime() default 10L;

TimeUnit timeUnit() default TimeUnit.SECONDS;
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package nbc.ticketing.ticket911.common.lock;
package nbc.ticketing.ticket911.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -17,4 +18,6 @@
long waitTime() default 10L;

long leaseTime() default 10L;

TimeUnit timeUnit() default TimeUnit.SECONDS;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package nbc.ticketing.ticket911.common.aop;

import java.lang.reflect.Method;
import java.util.UUID;

import org.aspectj.lang.ProceedingJoinPoint;
Expand All @@ -14,7 +13,8 @@

import lombok.RequiredArgsConstructor;

import nbc.ticketing.ticket911.common.lock.RedissonMultiLock;
import nbc.ticketing.ticket911.common.annotation.LettuceMultiLock;
import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock;
import nbc.ticketing.ticket911.infrastructure.lettuce.LettuceLockManager;
import nbc.ticketing.ticket911.domain.booking.exception.BookingException;
import nbc.ticketing.ticket911.domain.booking.exception.code.BookingExceptionCode;
Expand All @@ -25,13 +25,13 @@
public class LettuceLockAspect {
private final LettuceLockManager lockManager;

@Around("@annotation(redissonLock)")
public Object lock(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock) throws Throwable {
String key = resolveKey(joinPoint, redissonLock);
String lockKey = "lock:" + (redissonLock.group().isEmpty() ? "" : redissonLock.group() + ":") + key;
@Around("@annotation(lettuceLock)")
public Object lock(ProceedingJoinPoint joinPoint, LettuceMultiLock lettuceLock) throws Throwable {
String key = resolveKey(joinPoint, lettuceLock);
String lockKey = "lock:" + (lettuceLock.group().isEmpty() ? "" : lettuceLock.group() + ":") + key;
String lockValue = UUID.randomUUID().toString();

boolean locked = lockManager.tryLock(lockKey, lockValue, redissonLock.leaseTime() * 1000);
boolean locked = lockManager.tryLock(lockKey, lockValue, lettuceLock.leaseTime() * 1000);
if (!locked) {
throw new BookingException(BookingExceptionCode.LOCK_ACQUIRE_FAIL);
}
Expand All @@ -43,9 +43,8 @@ public Object lock(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock
}
}

private String resolveKey(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
private String resolveKey(ProceedingJoinPoint joinPoint, LettuceMultiLock lettuceLock) {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();

Expand All @@ -55,6 +54,6 @@ private String resolveKey(ProceedingJoinPoint joinPoint, RedissonMultiLock redis
}

ExpressionParser parser = new SpelExpressionParser();
return parser.parseExpression(redissonLock.key()).getValue(context, String.class);
return parser.parseExpression(lettuceLock.key()).getValue(context, String.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nbc.ticketing.ticket911.common.lock.RedissonMultiLock;
import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock;
import nbc.ticketing.ticket911.domain.lock.LockRedisService;
import nbc.ticketing.ticket911.infrastructure.redisson.exception.LockRedisException;
import nbc.ticketing.ticket911.infrastructure.redisson.exception.code.LockRedisExceptionCode;
Expand All @@ -43,11 +43,11 @@ public Object lock(ProceedingJoinPoint joinPoint) {
lockKeys = dynamicKeys.stream()
.map(key -> buildLockKey(annotation.group(), key))
.toList();

return lockRedisService.executeWithMultiLock(
lockKeys,
annotation.waitTime(),
annotation.leaseTime(),
annotation.timeUnit(),
joinPoint::proceed
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ public BookingResponseDto createBookingWithLock(AuthUser authUser, BookingReques
.toList();

return lockService.runWithLock(lockKeys, 10_000L,
() -> bookingService.createBookingLettuce(authUser, dto));
() -> bookingService.createBookingByLettuce(authUser, dto));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

import lombok.RequiredArgsConstructor;

import nbc.ticketing.ticket911.common.lock.RedissonMultiLock;
import nbc.ticketing.ticket911.common.annotation.LettuceMultiLock;
import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock;
import nbc.ticketing.ticket911.domain.auth.vo.AuthUser;
import nbc.ticketing.ticket911.domain.booking.dto.request.BookingRequestDto;
import nbc.ticketing.ticket911.domain.booking.dto.response.BookingResponseDto;
import nbc.ticketing.ticket911.domain.booking.entity.Booking;
import nbc.ticketing.ticket911.domain.booking.exception.BookingException;
import nbc.ticketing.ticket911.domain.booking.exception.code.BookingExceptionCode;
import nbc.ticketing.ticket911.domain.booking.service.BookingDomainService;
import nbc.ticketing.ticket911.domain.concert.service.ConcertDomainService;
import nbc.ticketing.ticket911.domain.concertseat.entity.ConcertSeat;
import nbc.ticketing.ticket911.domain.concertseat.service.ConcertSeatDomainService;
import nbc.ticketing.ticket911.domain.user.entity.User;
Expand All @@ -28,12 +28,75 @@ public class BookingService {

private final UserDomainService userDomainService;
private final BookingDomainService bookingDomainService;
private final ConcertDomainService concertDomainService;
private final ConcertSeatDomainService concertSeatDomainService;

@Transactional
public BookingResponseDto createBooking(AuthUser authUser, BookingRequestDto bookingRequestDto) {

User user = userDomainService.findActiveUserById(authUser.getId());

List<ConcertSeat> concertSeats = concertSeatDomainService.findAllByIdOrThrow(bookingRequestDto.getSeatIds());

bookingDomainService.validateBookable(concertSeats, LocalDateTime.now());
concertSeatDomainService.validateAllSameConcert(concertSeats);
concertSeatDomainService.validateNotReserved(concertSeats);

Booking booking = bookingDomainService.createBooking(user, concertSeats);

int totalPrice = booking.getTotalPrice();

userDomainService.minusPoint(user, totalPrice);
concertSeatDomainService.reserveAll(concertSeats);

return BookingResponseDto.from(booking);
}

@LettuceMultiLock(key = "#bookingRequestDto.seatIds", group = "concertSeat")
@Transactional
public BookingResponseDto createBookingByLettuce(AuthUser authUser, BookingRequestDto bookingRequestDto) {

User user = userDomainService.findActiveUserById(authUser.getId());

List<ConcertSeat> concertSeats = concertSeatDomainService.findAllByIdOrThrow(bookingRequestDto.getSeatIds());

bookingDomainService.validateBookable(concertSeats, LocalDateTime.now());
concertSeatDomainService.validateAllSameConcert(concertSeats);
concertSeatDomainService.validateNotReserved(concertSeats);

Booking booking = bookingDomainService.createBooking(user, concertSeats);

int totalPrice = booking.getTotalPrice();

userDomainService.minusPoint(user, totalPrice);
concertSeatDomainService.reserveAll(concertSeats);

return BookingResponseDto.from(booking);
}

@Transactional
public BookingResponseDto createBookingByMySQL(AuthUser authUser, BookingRequestDto bookingRequestDto) {
User user = userDomainService.findActiveUserById(authUser.getId());

List<ConcertSeat> concertSeats = concertSeatDomainService.findAllByIdForUpdate(bookingRequestDto.getSeatIds());

bookingDomainService.validateBookable(concertSeats, LocalDateTime.now());
concertSeatDomainService.validateAllSameConcert(concertSeats);
concertSeatDomainService.validateNotReserved(concertSeats);

Booking booking = bookingDomainService.createBooking(user, concertSeats);

int totalPrice = booking.getTotalPrice();

userDomainService.minusPoint(user, totalPrice);
concertSeatDomainService.reserveAll(concertSeats);

return BookingResponseDto.from(booking);

}

@RedissonMultiLock(key = "#bookingRequestDto.seatIds", group = "concertSeat")
@Transactional
public BookingResponseDto createBookingLettuce(AuthUser authUser, BookingRequestDto bookingRequestDto) {
public BookingResponseDto createBookingByRedisson(AuthUser authUser, BookingRequestDto bookingRequestDto) {

User user = userDomainService.findActiveUserById(authUser.getId());

Expand Down Expand Up @@ -105,24 +168,4 @@ public void canceledBooking(AuthUser authUser, Long bookingId) {
bookingDomainService.cancelBooking(booking);
}

@Transactional
public BookingResponseDto createBookingByMySQL(AuthUser authUser, BookingRequestDto bookingRequestDto) {
User user = userDomainService.findActiveUserById(authUser.getId());

List<ConcertSeat> concertSeats = concertSeatDomainService.findAllByIdForUpdate(bookingRequestDto.getSeatIds());

bookingDomainService.validateBookable(concertSeats, LocalDateTime.now());
concertSeatDomainService.validateAllSameConcert(concertSeats);
concertSeatDomainService.validateNotReserved(concertSeats);

Booking booking = bookingDomainService.createBooking(user, concertSeats);

int totalPrice = booking.getTotalPrice();

userDomainService.minusPoint(user, totalPrice);
concertSeatDomainService.reserveAll(concertSeats);

return BookingResponseDto.from(booking);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ public ResponseEntity<CommonResponse<BookingResponseDto>> createBooking(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestBody BookingRequestDto bookingRequestDto) {

BookingResponseDto bookingResponseDto = bookingFacade.createBookingWithLock(authUser, bookingRequestDto);
BookingResponseDto bookingResponseDto = bookingService.createBooking(authUser, bookingRequestDto);

return ResponseEntity.status(HttpStatus.CREATED)
.body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto));
}

@PostMapping("/lettuce")
public ResponseEntity<CommonResponse<BookingResponseDto>> createBookingLettuce(
public ResponseEntity<CommonResponse<BookingResponseDto>> createBookingByLettuce(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestBody BookingRequestDto bookingRequestDto) {

Expand All @@ -52,6 +52,28 @@ public ResponseEntity<CommonResponse<BookingResponseDto>> createBookingLettuce(
.body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto));
}

@PostMapping("/mysql")
public ResponseEntity<CommonResponse<BookingResponseDto>> createBookingByMySql(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestBody BookingRequestDto bookingRequestDto) {

BookingResponseDto bookingResponseDto = bookingService.createBookingByMySQL(authUser, bookingRequestDto);

return ResponseEntity.status(HttpStatus.CREATED)
.body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto));
}

@PostMapping("/redisson")
public ResponseEntity<CommonResponse<BookingResponseDto>> createBookingByRedisson(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestBody BookingRequestDto bookingRequestDto) {

BookingResponseDto bookingResponseDto = bookingService.createBookingByRedisson(authUser, bookingRequestDto);

return ResponseEntity.status(HttpStatus.CREATED)
.body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto));
}

@GetMapping
public ResponseEntity<CommonResponse<List<BookingResponseDto>>> getBookings(
@AuthenticationPrincipal AuthUser authUser) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package nbc.ticketing.ticket911.domain.lock;

import java.util.concurrent.TimeUnit;

public interface LockRedisRepository {
boolean lock(String key, long waitTime, long leaseTime);
boolean lock(String key, long waitTime, long leaseTime, TimeUnit timeUnit);

void unlock(String key);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package nbc.ticketing.ticket911.domain.lock;

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

import nbc.ticketing.ticket911.common.funtional.ThrowingSupplier;

public interface LockRedisService {
<T> T executeWithLock(String key, long waitTime, long leaseTime, ThrowingSupplier<T> action);
<T> T executeWithLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit, ThrowingSupplier<T> action);

<T> T executeWithMultiLock(List<String> key, long waitTime, long leaseTime, ThrowingSupplier<T> action);
<T> T executeWithMultiLock(List<String> key, long waitTime, long leaseTime, TimeUnit timeUnit,
ThrowingSupplier<T> action);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public class RedissonLockRedisRepository implements LockRedisRepository {
private final RedissonClient redissonClient;

@Override
public boolean lock(String key, long waitTime, long leaseTime) {
public boolean lock(String key, long waitTime, long leaseTime, TimeUnit timeUnit) {
RLock lock = redissonClient.getLock(key);
try {
return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
return lock.tryLock(waitTime, leaseTime, timeUnit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockRedisException(LockRedisExceptionCode.LOCK_INTERRUPTED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import org.springframework.stereotype.Service;

Expand All @@ -19,8 +20,9 @@ public class RedissonLockRedisService implements LockRedisService {
private final LockRedisRepository lockRedisRepository;

@Override
public <T> T executeWithLock(String key, long waitTime, long leaseTime, ThrowingSupplier<T> action) {
boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime);
public <T> T executeWithLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit,
ThrowingSupplier<T> action) {
boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime, timeUnit);
if (!locked) {
throw new LockRedisException(LockRedisExceptionCode.LOCK_TIMEOUT);
}
Expand All @@ -42,13 +44,14 @@ public <T> T executeWithLock(String key, long waitTime, long leaseTime, Throwing
}

@Override
public <T> T executeWithMultiLock(List<String> keys, long waitTime, long leaseTime, ThrowingSupplier<T> action)
public <T> T executeWithMultiLock(List<String> keys, long waitTime, long leaseTime, TimeUnit timeUnit,
ThrowingSupplier<T> action)
throws LockRedisException {
List<String> lockedKeys = new ArrayList<>();

try {
for (String key : keys) {
boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime);
boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime, timeUnit);
if (!locked) {
throw new LockRedisException(LockRedisExceptionCode.LOCK_TIMEOUT);
}
Expand Down
Loading
Loading