배경 및 필요성
동시성 문제를 해결하는 다양한 방법
DB에 pending 걸어놓기
-> 유효시간이 지난 예약 보류 정보 데이터를 batch를 통해 삭제해야 했기 때문에 DB에 부하가 걸릴것이라고 생각
synchronized 키워드 사용
-> 테스트 해보기로함
시스템 구성
spring-boot-starter-data-redis 3.2.5
H2 2.2.224 (Server 모드)
-> 임베디드 모드는 메모리에 올려서 사용하기 때문에 테스트 해봤더니 redis보다 더 빨랐음
public class Reservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDateTime startTime;
private LocalDateTime endTime;
}
public void save(Reservation reservation) throws IllegalArgumentException{
if (!overlapsWithExisting(reservation)) {
reservationRepository.save(reservation);
}
}
public class RedisReservation {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long start;
private long end;
}
public void save(RedisReservation reservation) throws JsonProcessingException {
String key = "reservation";
String s = objectMapper.writeValueAsString(reservation);
// 검증후 저장
if (getReservations().stream().noneMatch(r -> overlapsWithExisting(reservation, r))) {
redisTemplate.opsForList().rightPush(key, s);
}
}
public List<RedisReservation> getReservations() throws JsonProcessingException {
String key = "reservation";
// List<Reservation>으로 변환
List<Object> range = redisTemplate.opsForList().range(key, 0, -1);
return range.stream()
.map(o -> {
try {
return objectMapper.readValue(Objects.requireNonNull(o).toString(), RedisReservation.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
})
.collect(Collectors.toList());
}
@Test
public void 중복_예약_테스트() throws InterruptedException {
int threadCount = 2000;
CountDownLatch latch = new CountDownLatch(threadCount);
List<Long> executionTimes = new ArrayList<>();
saveTest(threadCount, executionTimes, latch);
latch.await(); // 모든 스레드가 종료될 때까지 기다림
// 모든 스레드의 실행 시간 평균 계산
double averageTime = executionTimes.stream().mapToDouble(Long::doubleValue).average().orElse(0.0);
System.out.println("평균 실행 시간: " + averageTime + "ms");
assertThat(reservationRepository.findAll().size()).isEqualTo(1);
}
private void saveTest(int threadCount, List<Long> executionTimes, CountDownLatch latch) {
for (int i = 0; i < threadCount; i++) {
Thread reservationThread = new Thread(() -> {
long start = System.nanoTime();
saveReservation();
long end = System.nanoTime();
long duration = (end - start) / 1_000_000; // 밀리초로 변환
executionTimes.add(duration);
System.out.println("스레드 실행 시간: " + duration + "ms");
latch.countDown();
});
reservationThread.start();
}
}
private void saveReservation() {
Reservation reservation = Reservation.builder()
.startTime(LocalDateTime.now())
.endTime(LocalDateTime.now().plusHours(2))
.build();
reservationService.save(reservation);
}
- CountDownLatch을 통해 각 스레드의 동작 시간, 평균 시간을 측정하였다
assertThat(reservationRepository.findAll().size()).isEqualTo(1);
위의 코드를 통해 중복되지 않고 1개만 예약되는지 확인하였다.
역시 중복 예약이 발생하였다.
public synchronized void save(Reservation reservation) throws IllegalArgumentException{
if (!overlapsWithExisting(reservation)) {
reservationRepository.save(reservation);
}
}ㅍ
@Test
public void 레디스_중복_예약() throws JsonProcessingException, InterruptedException {
redisService.clear();
//스레드
int threadCount = 2000;
CountDownLatch latch = new CountDownLatch(threadCount);
List<Long> executionTimes = new ArrayList<>();
redisSaveTest(threadCount, executionTimes, latch);
latch.await(); // 모든 스레드가 종료될 때까지 기다림
// 모든 스레드의 실행 시간 평균 계산
double averageTime = executionTimes.stream().mapToDouble(Long::doubleValue).average().orElse(0.0);
System.out.println("평균 실행 시간: " + averageTime + "ms");
assertThat(redisService.getReservations().size()).isEqualTo(1);
}
private void redisSaveTest(int threadCount, List<Long> executionTimes, CountDownLatch latch) {
for (int i = 0; i < threadCount; i++) {
Thread reservationThread = new Thread(() -> {
long start = System.nanoTime(); // 스레드별 작업 시작 시간 (나노초)
saveRedisReservation();
long end = System.nanoTime(); // 스레드별 작업 종료 시간 (나노초)
long duration = (end - start) / 1_000_000; // 밀리초로 변환
executionTimes.add(duration);
System.out.println("스레드 실행 시간: " + duration + "ms");
latch.countDown();
});
reservationThread.start();
}
}
private void saveRedisReservation() {
RedisReservation reservation = RedisReservation.builder()
.start(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))
.end(LocalDateTime.now().plusHours(2).toEpochSecond(ZoneOffset.UTC))
.build();
try {
redisService.save(reservation);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
성능 비교
응답 시간 비교
DB | 미적용 | synchronized 키워드 적용 |
---|---|---|
H2 | 1301ms | 5245ms |
Redis | 2758ms | 3461ms |
처리 능력 비교( 10 Time 수행 후 평균 )
DB | 중복 예약 수 |
---|---|
H2 | 21 |
Redis | 136 |