정합성 보장 | WAS 여러대에서 정합성 보장 |
쓰기 DB가 여러대에서 정합성 보장 | 실행 속도 (실험) | 작업 순서 처리 보장 | |
동시성 고려 X | X | X | X | 1m 1s | X |
synchronized | X | X | X | 4m 56s | O |
비관적 락 | O | O | X | 4m 32s | O |
낙관적 락 | O | O (#1) | X | 1m 5s | X |
Redisson 락 | O | O | O | 4m 44s | O |
#1 : 해줄 것으로 생각됨
동시성 고려를 하지 않은 경우
아래 이미지처럼 기대 예약 개수인 250개 보다 많은 366건이 예약됨을 알 수 있다
동시성 처리를 위해 synchronized를 사용한 경우
@Transactional
synchronized public long bookPerformance(...) {
//공연 예매 로직
}
synchronized 위치가 저기가 맞는지 모르겠다 (public 앞)
평상시 잘 쓸일이 없는 예약어라서... 눈감아주길...
우선 저렇게 처리하면 눈에 띄게 상당히 느려진다
또, 아래처럼 블록된 요청들이 대기열에 있다가 응답 시간 초과가 일어나는 현상이 있다
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30014ms.
커넥션 타임 아웃 시간을 길게 가져가서 저런 일이 없도록 만들어보자
(편의상 하루인 86400초로 잡았다)
spring.datasource.hikari.connection-timeout: 86400000
그러나 아래처럼 예상 외로 250개보다 많은 262개인 것을 확인했다 (테스트 실패)
저 현상을 설명할 수 있는 가설 중 하나는 Service Layer는 스프링에 의해 (CGLib) 프록시 객체로 wrapping되어 아래처럼 호출될 것으로 예상한다
class 프록시객체 {
private 원본객체_타입 target = ...
public 호출_메서드() {
try {
target.원_메서드호출(); // (1)
commmit(); (2)
} catch(..) {
log();
rollback();
}
}
}
프록시 호출로직을 거쳐서 원본 메서드 호출을 하는 이 짧은 찰나에 동시성 문제가 생긴 것으로 보인다
조금 더 구체적으로 예기하자면 첫번째 쓰레드가 (1)에 와서 처리되고 난 후 (2)를 수행하려는 찰나에
같은 내용(공연 좌석)의 요청에 해당하는 두번째 쓰레드가 (1)에 이미 진입을 해서 validation로직을 통과한 경우이다
이렇게 되면 동일한 내용을 가진 두 쓰레드 모두 예약에 성공하게 되어버린다
낙관적 락과 비관적 락
자, 그러면 다른 방법은 무엇일까? 낙관적 락과 비관적 락이 있다
낙관적 락의 경우 Application 레벨에서 제공하는 락으로 읽기까지 허용하지만, 읽고 쓰려는 순간 Version을 감지해서 원본 데이터 조작유무를 판별해서 예외를 발생한다
조금 더 자세히 얘기하자면 읽는 것을 허용하기에 여러 쓰레드가 동시에 요청이 가능하다
이렇게 되면 공연 예매나 강의 수강 신청 등 순서가 중요한 요청의 경우 먼저 처리될 요청이 모종의 이유로 늦게 처리되어 뒤 쓰레드에 밀려서 예외가 발생할 경우, 처음부터 다시 요청해야 한다
즉 사용자가 억울한 경우가 생길 수 있다, 따라서 이 경우에는 배제한다
비관적 락의 경우 읽는 것 조차 막는다. MySQL Select for Update를 떠올리면 된다. X락끼리는 호환이 되지 않는다. (다만 XLock을 건 Select문과 순수한 Select문끼리는 호환이 된다)
Select For Update를 걸어서 여러 요청을 받으면 순서대로 실행되므로 요청 순서대로 순서가 보장이 된다
다만 문제는... 느리단 것!
비관적 락을 사용한 경우
잠시 본문의 코드를 가져왔다
위 그림처럼 가장 첫줄에 JpaRepository에서 DB 데이터에 접근해서 읽는 로직이 있다
바로 그곳에 락을 갈기면 걸면 된다!
그럼 아래처럼 결과가 나오게 된다
synchronized block과 다르게 완벽히 처리될 수 있었던 이유는 commit이 되지 않으면 읽는 것 조차되지 않기 떄문이다
따라서 다른 쓰레드로부터의 데이터 쓰기를 원천적으로 봉쇄할 수 있다
낙관적 락을 사용한 경우
update되는 테이블(혹은 엔티티)에 version 필드를 생성하면 된다
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
...
public class PerformanceSeat extends BaseEntity {
...
@Version
private long version;
}
Redison Lock 사용하기
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 21a7db18-59d0-4f4d-a9bc-a9aeb9ee3728 thread-id: 1124
at org.redisson.RedissonBaseLock.lambda$unlockAsync0$2(RedissonBaseLock.java:290) ~[redisson-3.22.1.jar:3.22.1]
5초 -> 하루(86400초)로 변경
그 결과 위 에러가 나오지 않으면서 테스트를 통과했다
참고
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers#81-redlock
https://helloworld.kurly.com/blog/distributed-redisson-lock/
'Back-end > Spring Boot, JPA' 카테고리의 다른 글
[땅굴 조사] JPA 트랜잭션이 수행되기까지 (0) | 2024.02.01 |
---|---|
Spring Data Repository 확장 (짧음) (0) | 2023.12.13 |
ArgumentResolver처리 과정 + Spring Web MVC 흐름 (0) | 2023.06.20 |
Argument Resolver 등록 관련 디버깅 (feat. Spring MVC 흐름) (0) | 2023.06.20 |
p6spy로 로그 포맷팅 (0) | 2023.06.16 |
hi hello... World >< 가장 아름다운 하나의 해답이 존재한다
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!