[문제 상황 & 원인]
- 관리자가 상품 정보를 수정하는 동안, 사용자로부터의 구매 요청이 동시에 발생하는 과정에서
구매 로직이 성공적으로 처리되었음에도, 상품 수량의 동기화 문제가 발생하였습니다.
[해결 과정]
- Redis 분산락 사용
- 관리자가 상품 정보를 수정하는 과정에서 해당 상품에 대한 구매 요청을 일시적으로 대기 상태로 전환하고,
- 상품 정보의 변경 작업이 완료된 후에 구매 요청을 처리할 수 있도록 구현하였습니다.
Redis의 Redisson 라이브러리를 사용하여 pub/sub 방식의 분산락 구현
// DistributedLockAop.java
@Aspect
@Component
@Slf4j(topic = "DistributedLock 설정")
@AllArgsConstructor
public class DistributedLockAop {
private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;
@Around("@annotation(com.popcorntalk.global.annotation.DistributedLock)")
public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
String baseKey = distributedLock.lockName();
String dynamicKey = generateDynamicKey(signature.getParameterNames(), joinPoint.getArgs(),
distributedLock.identifier());
String key = baseKey + " : " + dynamicKey;
RLock lock = redissonClient.getFairLock(key);
log.info("{} - 락 획득 시도", key);
try {
boolean lockAcquired = lock.tryLock(distributedLock.waitTime(),
distributedLock.leaseTime(), distributedLock.timeUnit());
if (!lockAcquired) {
log.info("{} - 락 획득 실패", key);
throw new IllegalArgumentException(key + " - RLock 획득 실패");
}
log.info("{} - 락 획득 성공", key);
return aopForTransaction.proceed(joinPoint);
} finally {
try {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
log.info("{} - 락 반납", key);
} catch (IllegalMonitorStateException e) {
log.info(e + baseKey + dynamicKey);
}
}
}
- 상품의 수정, 삭제, 구매 중 다른 유저가 상품에 접근할 수 없도록 상품의 ID로 분산락 적용
- 설정 중 코드의 중복이 발생하여서 커스텀 어노테이션 생성하여 코드의 중복 최소화
// ExchangeServiceImpl.java, 상품 구매 로직 메서드
@Override
@DistributedLock(lockName = "product", identifier = "productId", waitTime = 60, leaseTime = 4)
public void createExchange(Long userId, Long productId) {
Product product = productService.getProduct(productId);
pointService.checkUserPoint(userId, product.getPrice());
productAmount(product);
pointService.deductPointForPurchase(userId, product.getPrice());
Exchange exchange = Exchange.createOf(userId, product.getId(), product.getVoucherImage());
exchangeRepository.save(exchange);
notificationService.notifyPurchase(userId, ADMIN_EMAIL, product.getVoucherImage());
}'트러블슈팅' 카테고리의 다른 글
| Feign Client 예외 처리 문제 (0) | 2025.01.22 |
|---|---|
| 토큰 검증 오류 (0) | 2024.05.08 |
| 스케쥴링 (0) | 2024.04.25 |
| 배포전 로컬 테스트 중 데이터베이스 연결 문제 (0) | 2024.04.19 |
| RedisConfig의 복수의 CacheManager 문제 (0) | 2024.04.15 |