Skip to content

Commit

Permalink
refactor: lettuce lock을 어노테이션으로 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
mirageoasis committed Aug 27, 2024
1 parent c205f94 commit e95c7b4
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,56 +1,26 @@
package com.thirdparty.ticketing.domain.ticket.service.proxy;

import com.thirdparty.ticketing.domain.common.ErrorCode;
import com.thirdparty.ticketing.domain.common.LettuceRepository;
import com.thirdparty.ticketing.domain.common.TicketingException;
import com.thirdparty.ticketing.domain.ticket.dto.request.SeatSelectionRequest;
import com.thirdparty.ticketing.domain.ticket.dto.request.TicketPaymentRequest;
import com.thirdparty.ticketing.domain.ticket.service.ReservationService;
import com.thirdparty.ticketing.global.lock.lettuce.LettuceLockAnnotation;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class LettuceReservationServiceProxy implements ReservationServiceProxy {

private final LettuceRepository lettuceRepository;
private final ReservationService reservationService;

private void performSeatAction(String seatId, Runnable action) {
int retryLimit = 5;
int sleepDuration = 300;
String lockPrefix = "seat:";
String lockKey = lockPrefix + seatId;
try {
while (retryLimit > 0 && !lettuceRepository.seatLock(lockKey)) {
retryLimit -= 1;
Thread.sleep(sleepDuration);
}

if (retryLimit > 0) {
action.run();
} else {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}

} catch (InterruptedException e) {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT, e);
} finally {
lettuceRepository.unlock(lockKey);
}
}

@Override
@LettuceLockAnnotation(key = "#seatSelectionRequest.seatId")
public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {
performSeatAction(
seatSelectionRequest.getSeatId().toString(),
() -> reservationService.selectSeat(memberEmail, seatSelectionRequest));
reservationService.selectSeat(memberEmail, seatSelectionRequest);
}

@Override
@LettuceLockAnnotation(key = "#ticketPaymentRequest.seatId")
public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {
performSeatAction(
ticketPaymentRequest.getSeatId().toString(),
() -> reservationService.reservationTicket(memberEmail, ticketPaymentRequest));
reservationService.reservationTicket(memberEmail, ticketPaymentRequest);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.context.annotation.Primary;

import com.thirdparty.ticketing.domain.common.EventPublisher;
import com.thirdparty.ticketing.domain.common.LettuceRepository;
import com.thirdparty.ticketing.domain.member.repository.MemberRepository;
import com.thirdparty.ticketing.domain.payment.PaymentProcessor;
import com.thirdparty.ticketing.domain.seat.repository.LettuceSeatRepository;
Expand All @@ -22,6 +21,7 @@

@Configuration
public class ReservationServiceContainer {

@Bean
@Primary
public ReservationService redissonReservationServiceProxy(
Expand All @@ -31,8 +31,8 @@ public ReservationService redissonReservationServiceProxy(

@Bean
public ReservationService lettuceReservationServiceProxy(
LettuceRepository lettuceRepository, ReservationRedisService reservationRedisService) {
return new LettuceReservationServiceProxy(lettuceRepository, reservationRedisService);
ReservationRedisService reservationRedisService) {
return new LettuceReservationServiceProxy(reservationRedisService);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.thirdparty.ticketing.global.lock;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class CustomSpringELParser {
private CustomSpringELParser() {}

public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}

return parser.parseExpression(key).getValue(context, Object.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.thirdparty.ticketing.global.lock.lettuce;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LettuceLockAnnotation {
String key(); // SpEL 표현식으로 Lock 키를 결정

int retryLimit() default 5; // 기본 재시도 횟수

int sleepDuration() default 300; // 기본 슬립 시간 (밀리초)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.thirdparty.ticketing.global.lock.lettuce;

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;

import com.thirdparty.ticketing.domain.common.ErrorCode;
import com.thirdparty.ticketing.domain.common.LettuceRepository;
import com.thirdparty.ticketing.domain.common.TicketingException;
import com.thirdparty.ticketing.global.lock.CustomSpringELParser;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Component
public class LettuceLockAspect {

@Autowired private LettuceRepository lettuceRepository;

private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();

private static final String LETTUCE_LOCK_PREFIX = "seat-lock-";

@Around("@annotation(com.thirdparty.ticketing.global.lock.lettuce.LettuceLockAnnotation)")
public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LettuceLockAnnotation lettuceLockAnnotation =
method.getAnnotation(LettuceLockAnnotation.class);
String lockKey =
LETTUCE_LOCK_PREFIX
+ CustomSpringELParser.getDynamicValue(
signature.getParameterNames(),
joinPoint.getArgs(),
lettuceLockAnnotation.key());

int retryLimit = lettuceLockAnnotation.retryLimit();
int sleepDuration = lettuceLockAnnotation.sleepDuration();

try {
while (retryLimit > 0 && !lettuceRepository.seatLock(lockKey)) {
retryLimit -= 1;
Thread.sleep(sleepDuration);
}

if (retryLimit > 0) {
return joinPoint.proceed();
} else {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}

} catch (InterruptedException e) {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT, e);
} finally {
lettuceRepository.unlock(lockKey);
}
}

private String parseSpel(String spel, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Object rootObject = joinPoint.getTarget(); // 메서드가 호출된 대상 객체
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(
rootObject,
methodSignature.getMethod(), // Method
joinPoint.getArgs(), // Method Arguments
new DefaultParameterNameDiscoverer() // Parameter Name Discoverer
);
return spelExpressionParser.parseExpression(spel).getValue(context, String.class);
}
}

0 comments on commit e95c7b4

Please sign in to comment.