이번 글의 초점은
Deadlock
과Lost Update
가 발생했던 원인을DB Lock 최소화
관점에서 해결해보는데에 있습니다.
혹여 이 글을 통해 제동시성 시리즈
를 접하시는 분들은 앞의 두 글을 참고하셔서 본인 프로젝트의 상황에 맞는 해결방안을 강구하시길 응원합니다.
Redis를 통해 해결하는 방안(분산락, Redis를 통한 재고관리 등)
은 다음 글에 이어질 예정입니다.
원활한 이해를 위해 파트 1은 이전 글의 내용을 가지고 왔습니다.
이전 글에서 분석한 데드락 발생 이유를 기억해보자
1.Product.Id
를 FK로 가지는OrderProduct Insert 쿼리
가 커밋되면서해당 Product 레코드에 공유락을 획득
한다.
2. Product Update를 위해배타락(Exclusive Lock)을 요청
한다.
3. 그러나 다른 트랜잭션들이 공유락을 걸어둔 상태이므로, 배타락을 획득하지 못하고 데드락에 걸린다.
공유락은 다른 트랜잭션이 보유하고 있더라도 획득할 수 있다.
-> 그렇기에 여러 트랜잭션들이 공유락을 보유하게 되고, 배타락 획득 요청이 다른 트랜잭션의 공유락으로 인해 거절당하면서 데드락이 발생하게 된다.
공유락보다 배타락이 먼저 획득되도록 한다면?
-> 배타락을 다른 트랜잭션에서 보유하고 있다면 대기상태에 들어가게 된다.
-> 당연히 이후 로직들이 실행되지 않기에 Product 레코드의 공유락 획득
도 밀리게 되면서
-> 배타락을 획득한 트랜잭션은 무사히 모든 작업을 수행하게 된다.
비관적 락에 비해 상대적으로 배타락 점유시간이 짧다.
-> 비관적 락은 Product Select 시점
부터 배타락을 점유하지만, 이 방법은 Product Update 시점
부터 배타락을 점유하는 것이므로 더 짧게 보유한다.
Product.reduceStock()
을 통해재고차감 및 예외발생
로직을 실행하고 있다.
영속성 컨텍스트에 의해 관리되는 Product이기에트랜잭션 커밋
직전에 더티체킹을 통해
Product Update 쿼리
가 작성되고 커밋되게 된다.
더티체킹 대신 직접 네임드쿼리를 작성하여 커밋해주면 된다.
-> 단, 부수적으로 발생하는 파급효과를 인지하고 고려하여 사용하자.
위와 같이
@Query, @Modfying
어노테이션을 통해 직접 Named Query를 작성해주면
쓰기지연저장소 저장 -> 트랜잭션 커밋
의 사이클과 별도로 쿼리를 커밋할 수 있다.
-> 데드락의 발생 원인이었던OrderProduct Insert -> Product Update
커밋 순서에서
Product Update
가 앞으로 오게 되면서
공유락으로 인해 배타락을 획득하지 못하던 데드락의 원인
을 해결할 수 있게 되었다!
.
Select 해온 Product의 재고를 기준으로 주문수량만큼 차감하고
차감한 후의 Product의 값들로 Update 쿼리를 커밋한다.
Select 시점과 Update 시점 사이에 발생한 변경사항(재고차감)과 무관하게
현재 트랜잭션의 Product 값들로 덮어쓰기를 해버린다.
타 트랜잭션들의 Update 내역들이 덮어쓰기된다. -> 갱신내역들이 손실된다.(Lost Update)
위 쿼리를 살펴보면,
DB Record
값을 기준으로 재고를 차감하고 있으며,
재고보다 주문수량이 많을 경우
에는 업데이트를 하지 않도록 Where절에 조건을 주고 있다.
-> Lost Update 해결 :)
.
기존에는
Prduct.reduceStock()
에서 재고부족 예외처리를 해주고 있었다.
위에서 작성한 네임드쿼리를 보면, 응답값으로int
를 받고 있었다.
이는해당 쿼리로 인해 변경된 레코드 수
를 return해주는 것이다.
-> 변경된 레코드 수가 0이라는 것은해당 id를 가진 Product가 없거나, 재고가 부족했다
라는 얘기인데
-> Product를 Select해오면서 시작되는 로직이기에 전자의 경우를 배제하고
int 값을 기준으로 재고부족 예외발생 로직
을 작성해 주었다.
--> 전자의 경우가 발생하려면 그 사이에 Product가 삭제됐다는 얘기인데, 그 때에도 예외가 발생하는 편이 낫다. 확실한 예외를 던지고 싶다면 if문을 중첩해서 existById()로 검증하고 예외를 던지면 될...듯 하다
도메인은 벌크업, 서비스는 다이어트
원칙을 지키고자 노력하지만..!네임드쿼리 실행 전 flush()가 발생한다는 점
-> 이부분은 아직 명확하게 실험해보진 못하여 정확하게 이렇다! 라고 설명드리진 못합니다.
(저는 재고차감 이전에 발생하는 쿼리가 없어 고려하지 않았습니다.)