์ฝ์ํธ ์๋น์ค์ ์ข์ ์์ฝ ๋ฐ ํฌ์ธํธ ์ถฉ์ ๊ธฐ๋ฅ์ ์ฌ๋ฌ ์ ์ ๊ฐ ๋์์ ์์ฒญ์ ๋ณด๋ผ ์ ์๋ ํ๊ฒฝ์์ ๋์ํฉ๋๋ค. ์ด๋, ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ๊ณผ ์ถฉ๋ ๋ฐฉ์ง๋ฅผ ์ํ ๋์์ฑ ์ ์ด๋ ํ์์ ์ ๋๋ค. ์ด ๊ธ์ ๋์์ฑ ์ ์ด ๋ฐฉ์์ผ๋ก ๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ์ ์ ์ฉํ ๊ฒฝ์ฐ์ ์ฑ๋ฅ์ ๋น๊ตํ์ฌ, ์ต์ ์ ๋์์ฑ ์ ์ด ๋ฐฉ์์ ๊ฒฐ์ ํ๊ธฐ ์ํด ์์ฑ๋์์ต๋๋ค.
@Test
public void ์ข์_์์ฝ_๋์์ฑ_์ ์ด() throws Exception {
// Given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
List<Future<ReserveSeatsResponseCommand>> futures = new ArrayList<>();
// When
for (int i = 0; i < threadCount; i++) {
final Long userId = (long) i + 1;
ReserveSeatsCommand command = new ReserveSeatsCommand(userId, concertId, scheduleId, seatIds);
Future<ReserveSeatsResponseCommand> future = executorService.submit(() -> {
try {
return reservationFacade.reserveSeats(command);
} catch (Exception e) {
log.error("Reservation failed: {}", e.getMessage());
return null;
}
});
futures.add(future);
}
// ์ฑ๊ณต ๋ฐ ์คํจ ์นด์ดํฐ
int successCount = 0;
int failureCount = 0;
// ์ค๋ ๋๊ฐ ์๋ฃ๋ ๋๊น์ง ๋๊ธฐ
for (Future<ReserveSeatsResponseCommand> future : futures) {
ReserveSeatsResponseCommand reservation = future.get();
if (reservation != null) {
successCount++;
log.info("Reservation successful: {}", reservation.getReservationId());
} else {
failureCount++;
}
}
log.info("successCount: {}", successCount);
log.info("failureCount: {}", failureCount);
// Then
assertEquals(1, successCount);
assertEquals(99, failureCount);
// ๋ชจ๋ ์ค๋ ๋๊ฐ ์์
์ ์๋ฃํ ํ ์์ฝ ์ํ ํ์ธ
List<Seat> reservedSeats = seatRepository.findSeatsByScheduleId(seatIds, scheduleId);
reservedSeats.forEach(seat -> {
assertEquals(seat.getStatus(), TEMPORARILY_RESERVED);
});
executorService.shutdown();
}
ํ ์คํธ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋๊ด์ ๋ฝ์ ์ฌ์ฉํ ๊ฒฝ์ฐ๋ ์ฌ์๋ ๊ธฐ๋ฅ์ด ์์ผ๋ฏ๋ก, ์ถฉ๋์ด ๋ฐ์ํ์ง ์๋ ๊ฒฝ์ฐ์ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๋ฐ๋ฉด, ๋น๊ด์ ๋ฝ์ ๋๊ธฐ ํ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก, ์ ์ฒด ์คํ ์๊ฐ์ด ๋ ๊ฑธ๋ ธ์ต๋๋ค. ๋ฐ๋ผ์, ์คํจ ์ ์ฌ์๋ํ๋ ๊ธฐ๋ฅ์ด ์๋ ์ํฉ์์๋ ๋๊ด์ ๋ฝ์ด ๋ ํจ์จ์ ์ด๋ผ๋ ๊ฒฐ๋ก ์ ๋ด๋ ธ์ต๋๋ค.
@Test
void ํฌ์ธํธ_์ถฉ์ _๋์์ฑ_์ ์ด() throws InterruptedException {
int THREAD_COUNT = 1000;
RechargeCommand command = new RechargeCommand(1L, 100L);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
pointService.rechargePoint(command);
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
Point point = pointRepository.point(1L);
// DB์๋ 1000 ํฌ์ธํธ๊ฐ ์ ์ฅ๋์ด์์ต๋๋ค.
assertThat(point.getPointAmount()).isEqualTo(new BigDecimal("101000.00"));
}
ํ ์คํธ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
ํฌ์ธํธ ์ถฉ์ ๊ธฐ๋ฅ์ ๊ฒฝ์ฐ, ๋์์ฑ ์ ์ด๋ฅผ ํ๋ฉด์ ๋ชจ๋ ์์ฒญ์ ์ถฉ์ ์ด ๋ณด์ฅ๋์ด์ผ ํ๋ ์๋๋ฆฌ์ค์ ๋๋ค. ๋ง์ฝ ๋์์ 10๋ฒ์ ์ถฉ์ ์์ฒญ์ด ๋ค์ด์จ๋ค๋ฉด, 10๋ฒ ๋ชจ๋ ์ถฉ์ ์ด ๋์ด์ผ ํฉ๋๋ค.
๋น๊ด์ ๋ฝ์ ๋๊ธฐ ํ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก, ์ถฉ๋์ด ๋ฐ์ํ ๊ฒฝ์ฐ ์ฆ์ ๋ค๋ฅธ ์ค๋ ๋๊ฐ ๋๊ธฐํ๋ ๊ตฌ์กฐ์
๋๋ค. ๋ฐ๋ฉด, ๋๊ด์ ๋ฝ์ ์ถฉ๋ ํ ์ฌ์๋ ์์
์ ์ํํ๊ธฐ ๋๋ฌธ์ ์๊ฐ์ด ๋ ๊ฑธ๋ ธ์ต๋๋ค. ์ฌ์๋ ํ์๋ฅผ @Retryable(value = {OptimisticLockingFailureException.class}, maxAttempts = 10, backoff = @Backoff(delay = 100))
๋ก ์ค์ ํ์ง๋ง, ์ด๋ก ์ธํด ํจ์จ์ฑ์ด ๋จ์ด์ง๋ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์์ต๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก, ๋น๊ด์ ๋ฝ์ด ๋ ๋น ๋ฅธ ์ฑ๋ฅ์ ๋ณด์์ผ๋ฉฐ, ๋๊ด์ ๋ฝ์ ์ฌ์๋ ๋ก์ง์ผ๋ก ์ธํด ์คํ๋ ค ๋นํจ์จ์ ์ด์์ต๋๋ค.
๋ ๋์ค ๋ถ์ฐ๋ฝ์ ๋ค์ ๊ธ์ ์ฐธ๊ณ ํ์ฌ ์ค์ ํ์์ต๋๋ค
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RedissonLockAspect {
private final RedissonClient redissonClient;
@Around("@annotation(com.hhplu.hhplusconcert.common.annotation.RedissonLock)")
@Transactional
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
String lockKey = method.getName() + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), annotation.value());
RLock lock = redissonClient.getLock(lockKey);
try {
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);
if (!lockable) {
log.info("Lock ํ๋ ์คํจ={}", lockKey);
throw new Exception("Lock ํ๋ ์คํจ");
}
log.info("๋ก์ง ์ํ");
return joinPoint.proceed();
} catch (InterruptedException e) {
log.info("์๋ฌ ๋ฐ์");
throw e;
} finally {
log.info("๋ฝ ํด์ ");
lock.unlock();
}
}
}
@Service
@RequiredArgsConstructor
public class PointFacade {
private final PointService pointService;
@RedissonLock(value = "#command.getUserId()")
public GetPointCommand rechargePoint(RechargeCommand command) {
return pointService.rechargePoint(command);
}
}
Redis(Redisson) ๋ถ์ฐ๋ฝ์ ํ์ฉํ์ฌ ๋์์ฑ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ
๋ถ์ฐ ๋ฝ์ ํตํด 1000๊ฐ์ ์ค๋ ๋๊ฐ ๋์์ ํฌ์ธํธ ์ถฉ์ ๋ก์ง์ ์ํํ๋ ํ ์คํธ๋ฅผ ์งํํ ๊ฒฐ๊ณผ, ๋ฝ ํ๋ ๋ฐ ํด์ ์ ๋ฐ์ํ๋ ์ค๋ฒํค๋๋ก ์ธํด ์ ์ฒด ์คํ ์๋๊ฐ ๋ค์ ๋๋ ค์ง๋ ๊ฒ์ ํ์ธํ์ต๋๋ค. Redisson์ ๋ถ์ฐ ๋ฝ์ ๋ถ์ฐ ํ๊ฒฝ์์ ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๋ ๋ฐ ์ ์ฉํ์ง๋ง, ๋์ ํธ๋ํฝ ์ํฉ์์ ๋ฐ์ํ๋ ๋ฝ ํ๋ ์๋์ ๊ฒฝ์์ผ๋ก ์ธํด ์ง์ฐ์ด ๋ฐ์ํ ์ ์๋ค๋ ์ ์ ํ์ ํ์ต๋๋ค.
๋น๊ด์ ๋ฝ๊ณผ ๋๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ ๋์ผํ ํ ์คํธ๋ฅผ ์งํํด๋ณธ ๊ฒฐ๊ณผ, ๊ฐ ๋ฐฉ๋ฒ์ ๋ฐ๋ฅธ ์ฑ๋ฅ ์ฐจ์ด๋ฅผ ๋น๊ตํ ์ ์์์ต๋๋ค. ๋น๊ด์ ๋ฝ์ ๊ฒฝ์ฐ, ์์ ์ถฉ๋์ด ์์๋๋ ์ํฉ์์ ์์ ์ ์ธ ๋์์ฑ ์ ์ด๊ฐ ๊ฐ๋ฅํ์ผ๋, ๋ฐ์ดํฐ ์ถฉ๋์ด ๋ฐ์ํ์ง ์๋ ๊ฒฝ์ฐ์๋ ๋ถํ์ํ ๋ฝ ์ ์ ๋ก ์ธํ ์ฑ๋ฅ ์ ํ๊ฐ ์์์ต๋๋ค. ๋ฐ๋ฉด, ๋๊ด์ ๋ฝ์ ๋ฐ์ดํฐ ์ถฉ๋์ด ์ ์ ํ๊ฒฝ์์ ๋น ๋ฅธ ์ฑ๋ฅ์ ๋ณด์์ง๋ง, ์ถฉ๋์ด ๋ฐ์ํ๋ฉด ๋กค๋ฐฑ์ ๋ฐ๋ณตํด์ผ ํ๋ฏ๋ก ๋ฆฌ์์ค ์๋ชจ๊ฐ ์ปค์ง ์ ์๋ค๋ ์ ์ ๋ฐ๊ฒฌํ์ต๋๋ค.
์ด๊ธฐ ์ค๊ณ ๋ฐฉ์นจ์ ๋ฐ๋ผ, ์์ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ ์ข์ ์์ฝ ์์คํ ์๋ ๋๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ, ๋ถํ์ํ ๋ฝ ์ ์ ๋ฅผ ์ต์ํํ๋ฉฐ ๋น ๋ฅธ ์ฑ๋ฅ์ ๋ณด์ฅํ๊ธฐ๋ก ํ์ต๋๋ค. ๋ฐ๋ฉด, ํฌ์ธํธ ์ถฉ์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ์ฌ, ์ถฉ๋์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋ฎ์์๋ ๋ถ๊ตฌํ๊ณ ์ผ๊ด์ฑ ์ ์ง๊ฐ ์ค์ํ ๊ธฐ๋ฅ์ ์์ ์ ์ธ ๋์์ฑ ์ ์ด๋ฅผ ๋์ ํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. ๋ถ์ฐ ๋ฝ์ ํฅํ ํ์ฅ์ฑ์ด ํ์ํ ํ๊ฒฝ์ด๋ ๋ณ๋์ ๋คํธ์ํฌ ๋ถ์ฐ ํ๊ฒฝ์์ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์์ ๊ฒ์ผ๋ก ํ๋จํ์ฌ, ์ํฉ์ ๋ง๊ฒ ์ ์ฉ ๋ฐฉ์์ ๊ณ ๋ คํ ๊ณํ์ ๋๋ค.