트랜잭션 격리수준을 높여 원자적 연산을 할 수 있을까?

홍지범·2023년 6월 12일
0

Intro

동시성 문제를 해결하기 위해서는 원자적 연산이 필요합니다.
그래서 트랜잭션을 이용해 작업 단위를 하나의 수행처럼 보장받으려 했습니다.

이걸로 모든 문제가 해결된 것 일까요?

부하를 테스트할 수 있는 ngrinder로 구매 api를 테스트 해보겠습니다.

테스트 제약사항

  • 1인당 구매 제한량 1개로 설정
  • random memberId로 같은 멤버가 동일한 제품에 구매요청 가능하지만 중복 구매는 불가능

문제 발생

중복 구매가 발생한 내용

purchaseIdmemberUniqueIdproductIdamounttimestamp
55461112023-05-08 14:03:44
55471112023-05-08 14:03:44
55481112023-05-08 14:03:44
55491112023-05-08 14:03:44
55501112023-05-08 14:03:44
55511112023-05-08 14:03:44
55521112023-05-08 14:03:44
55531112023-05-08 14:03:44
55541112023-05-08 14:03:44

문제 분석

  • 인당 구매 제한량을 1개로 설정했음에도 중복 구매가 발생함
  • 설정된 재고보다 초과된 구매 이력 발생함
  • 즉, 트랜잭션을 적용했음에도 동시성 문제 발생

왜 트랜잭션을 적용 했음에도 동시성 문제가 발생한 것 일까요?

문제에 대해 얘기하기 전 MySQL client로 가 다음과 같은 명령어를 입력해보겠습니다.

mysql > SELECT @@GLOBAL.transaction_isolation;

Variable_nameValue
transaction_isolationREPEATABLE-READ

혹은
mysql > SHOW VARIABLES LIKE '%isolation';

@@session.transaction_isolation
REPEATABLE-READ

디폴트로 적용된 MySQL이 트랜잭션 격리 수준(transantion_isolation) 은 REPEATABLE-READ 입니다.

트랜잭션 격리 수준

트랜잭션 격리 수준이란 특정 트랜잭션이 다른 트랜잭션에서 변경한 내용을 볼 수 있는지에 대한 격리 혹은 고립 여부를 의미 합니다.
그리고 격리 수준은 아래와 같이 나뉩니다.

MySQL 트랜잭션 격리 수준

DIRTY READNON-REPEATABLE RREADPHANTHOM 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을 볼 뿐 입니다.

따라서 아래와 같은 그림처럼 작동합니다.

REPETABLE READ 동작 방식

seqA 스레드iphone14B 스레드
0start transaction남은 재고 : 2
1재고 read : 2남은 재고 : 2start transaction
2남은 재고 : 2재고 read : 2(B 스레드의 스냅샷)
3재고 차감&상품 update : 1남은 재고 : 1
4commit남은 재고 : 1
5남은 재고 : 1재고 read : 2(B 스레드의 스냅샷)
6남은 재고 : 1재고 차감&상품 update : 1

seq 5. 처럼 A 스레드가 커밋을 해도 B는 자신의 스냅샷을 읽는다.

SERIALIZABLE

그렇다면 격리수준을 SERIALIZABLE(직렬화 가능) 으로 높여보면 어떨까요?
SERIALIZABLE은 Shared Lock(공유 락, 읽기 락)을 걸어 다른 트랜잭션이 접근할 수 없도록 합니다.
다른 트랜잭션이 해당 레코드에 접근할 수 없다면 수행을 온전히 끝낼 수 있지 않을까요?

스프링에서는 이렇게 적용 할 수 있습니다.

    @Transactional(isolation=Isolation.SERIALIZABLE)
    public void purchase(PurchaseRequest purchaseRequest) {
		...
    }

결론

  • REPETABLE READ의 격리 수준으로는 원자적 연산을 할 수 없다.
  • 격리수준을 높인다면 어떻게 될까?
profile
왜? 다음 어떻게?

0개의 댓글