동시성 문제를 해결하기 위해서는 원자적 연산이 필요합니다.
그래서 트랜잭션을 이용해 작업 단위를 하나의 수행처럼 보장받으려 했습니다.
이걸로 모든 문제가 해결된 것 일까요?
부하를 테스트할 수 있는 ngrinder로 구매 api를 테스트 해보겠습니다.
테스트 제약사항
- 1인당 구매 제한량 1개로 설정
- random memberId로 같은 멤버가 동일한 제품에 구매요청 가능하지만 중복 구매는 불가능
중복 구매가 발생한 내용
purchaseId memberUniqueId productId amount timestamp 5546 1 1 1 2023-05-08 14:03:44 5547 1 1 1 2023-05-08 14:03:44 5548 1 1 1 2023-05-08 14:03:44 5549 1 1 1 2023-05-08 14:03:44 5550 1 1 1 2023-05-08 14:03:44 5551 1 1 1 2023-05-08 14:03:44 5552 1 1 1 2023-05-08 14:03:44 5553 1 1 1 2023-05-08 14:03:44 5554 1 1 1 2023-05-08 14:03:44
왜 트랜잭션을 적용 했음에도 동시성 문제가 발생한 것 일까요?
문제에 대해 얘기하기 전 MySQL client로 가 다음과 같은 명령어를 입력해보겠습니다.
mysql > SELECT @@GLOBAL.transaction_isolation;
Variable_name Value transaction_isolation REPEATABLE-READ 혹은
mysql > SHOW VARIABLES LIKE '%isolation';
@@session.transaction_isolation REPEATABLE-READ
디폴트로 적용된 MySQL이 트랜잭션 격리 수준(transantion_isolation) 은 REPEATABLE-READ 입니다.
트랜잭션 격리 수준이란 특정 트랜잭션이 다른 트랜잭션에서 변경한 내용을 볼 수 있는지에 대한 격리 혹은 고립 여부를 의미 합니다.
그리고 격리 수준은 아래와 같이 나뉩니다.
MySQL 트랜잭션 격리 수준
DIRTY READ NON-REPEATABLE RREAD PHANTHOM READ READ UNCOMMITED 발생 발생 발생 READ COMMITED 없음 발생 발생 REPEATABLE READ 없음 없음 발생(InnoDB는 없음) SERIALIZABLE 없음 없음 없음
- DIRTY READ : A 세션에서 완료(commit or rollback) 되지 않은 트랜잭션의 변경 내용을 B 세션에서도 볼 수 있는 경우. 내용이 계속 변할 수 있음.
- NON-REPEATABLE READ : REPEATABLE READ가 불가능. 즉, 한 트랜잭션 내에서는 같은 쿼리를 실행했을 때 같은 결과를 가져와야 함에도 불구하고 다른 세션의 트랜잭션 종료 여부에 따라 select 한 결과가 다르게 나올 수 있음.
- PHANTHOM READ : insert에 의해서만 발생하며 한 트랜잭션 내에서 같은 쿼리를 두 번 실행했을 때 첫 번째 결과에서는 없던 레코드가 두 번째 결과에서 나타나는 경우
현재는 InnoDB 엔진의 REPEATABLE READ 사용하기 때문에 PHANTOM READ가 발생하지 않습니다.(한 트랜잭션 내에서 읽기 연산을 했을 때 결과가 동일하게 나온다는 뜻)
REPETABLE READ 트랜잭션은 처음 read operation을 한 시간을 기록해 해당 시점을 기준으로 consistent read를 수행 합니다.
따라서 다른 트랜잭션이 commit 하더라도 새로 commit 된 데이터는 보지 않습니다. 오직 첫 read의 snapshot을 볼 뿐 입니다.
따라서 아래와 같은 그림처럼 작동합니다.
seq A 스레드 iphone14 B 스레드 0 start transaction 남은 재고 : 2 1 재고 read : 2 남은 재고 : 2 start transaction 2 남은 재고 : 2 재고 read : 2(B 스레드의 스냅샷) 3 재고 차감&상품 update : 1 남은 재고 : 1 4 commit 남은 재고 : 1 5 남은 재고 : 1 재고 read : 2(B 스레드의 스냅샷) 6 남은 재고 : 1 재고 차감&상품 update : 1 seq 5. 처럼 A 스레드가 커밋을 해도 B는 자신의 스냅샷을 읽는다.
그렇다면 격리수준을 SERIALIZABLE(직렬화 가능) 으로 높여보면 어떨까요?
SERIALIZABLE은 Shared Lock(공유 락, 읽기 락)을 걸어 다른 트랜잭션이 접근할 수 없도록 합니다.
다른 트랜잭션이 해당 레코드에 접근할 수 없다면 수행을 온전히 끝낼 수 있지 않을까요?
스프링에서는 이렇게 적용 할 수 있습니다.
@Transactional(isolation=Isolation.SERIALIZABLE)
public void purchase(PurchaseRequest purchaseRequest) {
...
}
- REPETABLE READ의 격리 수준으로는 원자적 연산을 할 수 없다.
- 격리수준을 높인다면 어떻게 될까?