쇼핑몰 재고관리 로직에서 발생한 동시성 문제 원인분석: 데드락, 갱신손실(Lost Update)

조성현·2023년 8월 7일
1

프로젝트 깃허브 [링크]

1. 테스트 환경

1-1. 현재 성덕스토어의 주문생성 로직

  1. 주문할 Product List 조회
  2. reduceProductsStock()을 통해 -> product.reduceStock() 메서드를 호출하고 더티체킹을 통해 재고차감
  3. Order 생성
  4. OrderProduct 생성

1-2. 동시성 테스트를 진행한 방법

1. line 90 -> 2000개의 스레드환경 세팅
2. line 91 -> 2000회 카운팅을 위한 CountDownLatch
3. line 94, 105 -> 스레드에게 작업부여
4. line 100, 111 -> 스레드마다 작업종료후 카운트차감
5. line 115 -> 2000회 카운팅이 종료된 이후에 테스트 검증로직이 작동할 수 있도록 기다려! 해주는 역할


2. 문제상황

2-1. 발생한 문제 파악

  1. 100개의 스레드에서 동시에 1개씩 주문을 넣었을 때, 30~40개의 Order & OrderProduct 레코드가 생성된다.
  2. 심지어 Product.stock -> 상품재고는 10개정도밖에 차감되지 않는다.
  3. 데드락이 발생한다.

3. 원인분석

간략한 정리

  1. Order & OrderProduct 레코드가 30~40개만 생성된 이유
  • 요약 -> Deadlock이 발생하고, DB에서 해당 트랜잭션들을 롤백시켜서!
    .
  • 프로젝트에서 사용중인 Maria DBInnoDB 스토리지 엔진을 사용한다.
    해당 엔진은 timeOut의 기본값이 50초(링크), 대기중인 트랜잭션 수(기본값 200)로 데드락을 판단하여 개입한다 (하나씩 롤백하면서 해결).
  1. 상품 재고가 10개 내외만 차감된 이유
  • 조회시점update쿼리 트랜잭션 커밋시점의 차이로 인해
    다른 트랜잭션의 재고 update가 반영되지 않은 데이터를 기준으로 더티체킹이 이루어졌기 때문.
  • 갱신 손실(Lost Update) 이라는 현상

3-1. 그렇다면 데드락이 왜 발생했을까?

  • 결론부터 말하자면 더티체킹의 작동방식과 OrderProduct Insert 쿼리로 인한 공유락이 맞물려서 발생한 것이다. (정확히는 공유락과 배타락이 맞물린 것)

  • 글 도입부에 첨부된 로직을 보면 상품 재고차감이 먼저 이루어지는 것처럼 보이지만, 실제로 더티체킹이 이루어지는 시점은 트랜잭션 커밋 직전이다.

  • 이에 따라 트랜잭션 커밋이 진행되는 순서는
    Order Insert -> OrderProduct Insert -> Product Update 순으로 진행된다.

  • 공유락(Shared Lock)은 어디서 발생한 것인가?
    -> MariaDB와 동일한 InnoDB 스토리지 엔진을 사용하는 MySQL DB의 자료를 확인해보았더니 ProductId를 FK로 가지는 OrderProduct Insert에서 공유락이 발생한 것을 확인할 수 있었다.

    FOREIGN KEY제약 조건이 테이블에 정의된 경우 제약 조건을 확인해야 하는 모든 삽입, 업데이트 또는 삭제는 제약 조건을 확인하기 위해 확인하는 레코드에 공유 레코드 수준 잠금을 설정합니다 . InnoDB또한 제약 조건이 실패하는 경우 이러한 잠금을 설정합니다 [Ref 하이라이트 링크]

  • 위 사진과 같이 여러 트랜잭션에서 OrderProduct Insert를 하면서 해당 Product Record에 공유락(S-Lock)을 획득한뒤,
  • Product Update를 위해 배타락(Exclusive Lock)을 요청한다.
  • 그러나 다른 트랜잭션들이 공유락을 걸어둔 상태이므로, 배타락을 획득하지 못하고 다른 트랜잭션이 종료되기를 기다리기 시작한다.
  • 문제는 다른 트랙잭션도 서로가 끝나기만을 기다리는 상황이기에 결국 Inno DB가 개입해서 트랜잭션들을 롤백 시키면서 데드락을 해결한다.

MySQL 공식문서[링크]에 따르면 대기목록 허용치를 초과한 상황에서
대기 목록 확인을 시도하는 트랜잭션은 롤백되고, 트랜잭션을 하나씩 롤백해나가면서 데드락을 해결해나간다고 한다.

  • 이런 전개로 30~40개 정도의 Order&OrderProduct Pair만 생성되었다고 보여진다.

3-2 갱신손실(Lost Update)은 왜 발생했을까?

  • 운좋게 데드락 or 롤백을 피한 트랜잭션들 30~40개가 커밋되었는데
    왜 상품재고는 10개 내외만 차감됐을까?
  • 그 이유는 위 사진과 같이 다른 트랜잭션의 update 쿼리가 커밋되기 전에 상품을 조회해오고(100개)
    -> 그 값에서 주문수량만큼 재고를 차감한뒤(99개)
    -> 그 결과를 더티체킹을 통해 Update Query Commit 했기 때문이다.

조회시점과 Update 시점의 차이로 인해
조회해온 Product와 DB의 Product 간의 데이터 정합성이 깨지게 되면서
갱신손실(Lost Update)이 발생


지금까지 쇼핑몰 재고관리 로직에서 발생한 동시성문제
문제상황 파악 및 원인분석을 해보았습니다.
다음 글에서는 이러한 동시성문제를 어떻게 해결할 수 있을지 학습하여 정리해보겠습니다.
감사합니다.

profile
맛있는 음식과 여행을 좋아하는 당당한 뚱땡이

0개의 댓글