이전 편은 DB LOCK을 사용해서 동시성 문제를 해결해 보았다.
하지만 문제점은 DB와 서버 과부하였다.
그래서 이 부담을 줄이기 위해 Redisson(분산 락)을 사용해서 해결할 것이다.
Redisson이란?
Redis를 기반으로 한 Java 클라이언트 라이브러리다.
Redis를 사용해서 다양한 기능과 데이터 구조를 제공하고, 다른 프로세스 또는 서버 간에 데이터를 공유할 수 있다.
그리고 여기서 사용하게 되는 분산 락도 제공하여 데이터 처리와 분산 작업을 간편하게 처리할 수 있다.
코드
(JPA 사용)
TicketController
@RestController
@RequiredArgsConstructor
@RequestMapping("/ticket")
@Slf4j
public class TicketController {
private final TicketService ticketService;
private final TicketReserveRedissonService ticketReserveRedissonService;
private final TicketKafkaService ticketKafkaService;
// step 2) 레디스 분산락(redisson)
@PostMapping("/redis")
public ResponseEntity<String> reserveTicketWithRedis(@RequestBody TicketReserveRequest ticketReserveRequest) throws TicketSoldOutException {
log.info("POST " + ticketReserveRequest.getTicketId() + ", " + LocalDateTime.now());
ticketReserveRedissonService.reverseTicket(ticketReserveRequest.getTicketId());
return ResponseEntity.ok("티켓 예약 성공");
}
// 티켓 생성
@PostMapping
private ResponseEntity<String> autoCreateTicket(@RequestBody TicketRequest ticketRequest) {
ticketService.save(ticketRequest);
return ResponseEntity.ok("티켓 생성 성공");
}
}
TicketReserveRedissonService
@RequiredArgsConstructor
@Slf4j
@Service
public class TicketReserveRedissonService {
private final RedissonClient redissonClient;
private final TicketService ticketService;
// step 2) redisson 분산 락
public void reverseTicket(Long ticketId) {
RLock lock = redissonClient.getLock("ticket-" + ticketId);
try {
boolean isLocked = lock.tryLock(3, 3, TimeUnit.SECONDS);
if (!isLocked) {
throw new LockException();
}
ticketService.reserveTicket(ticketId);
} catch (InterruptedException e) {
log.error(e.getMessage());
} finally {
lock.unlock();
}
}
}
RedissonClient를 사용해서 getLock을 통해 락을 생성 또는 가져오고, tryLock을 통해 Lock을 시도하는 것이다. 만약 해당 락이 사용 중이면 3초 동안 대기하는데 그 시간 동안 락을 얻지 못하면 throw를 던진다.
그래서 이렇게 동시성 문제를 해결할 것이다.
TickerService
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class TicketService {
private final TicketRepository ticketRepository;
public Long save(TicketRequest ticketRequest) {
return ticketRepository.save(Ticket.builder()
.ticketName(ticketRequest.getTicketName())
.ticketMaxCount(ticketRequest.getTicketMaxCount())
.build()).getId();
}
public TicketResponse findById(Long id) {
return TicketResponse.from(ticketRepository.findById(id).orElseThrow(TicketNotFoundException::new));
}
// step 2) Redisson
public void reserveTicket(Long ticketId) {
Ticket ticket = ticketRepository.findById(ticketId).orElseThrow(TicketNotFoundException::new);
ticket.currentCountIncrement();
log.info("잔여 티켓: " + (ticket.getTicketMaxCount() - ticket.getTicketCurrentCount()));
}
}
테스트 코드
TicketServiceTest
@SpringBootTest
class TicketServiceTest {
@Autowired
private TicketService ticketService;
@Autowired
private TicketReserveRedissonService ticketReserveRedissonService;
private Long id1 = null;
private final Integer maxCount = 100;
@BeforeEach
public void before() {
TicketRequest ticket1 = new TicketRequest("ticket1", maxCount);
id1 = ticketService.save(ticket1);
}
@Test
void 동시성_문제_with_Redis() throws InterruptedException {
int peopleCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch tt = new CountDownLatch(peopleCount);
for (int i = 0; i < peopleCount; i++) {
executorService.execute(() -> {
ticketReserveRedissonService.reverseTicket(id1);
tt.countDown();
});
}
tt.await();
TicketResponse ticket = ticketService.findById(id1);
Assertions.assertThat(ticket.getTicketCurrentCount()).isEqualTo(maxCount);
}
}
실행 결과
이렇게 해서 Redisson을 사용해서 동시성 문제를 해결할 수 있다.
그리고 이렇게 했을 경우 만약 MSA 환경을 구축할 정도로 트래픽이 많으면 서버에 부담이 되기 때문에 다음 step에서는 카프카를 사용해서 메시지 분산처리를 해볼 것이다.
다음 step)
'JAVA > Redis' 카테고리의 다른 글
[Redis/kafka] 데이터베이스 동시성 문제 해결 코드 (STEP 4. Redis, Redisson, kafka로 과부하 줄이기) (0) | 2023.07.07 |
---|---|
[Redis] 도커 컴포즈로 Redis와 RedisInsight 설치해보자. (0) | 2023.07.05 |