본문 바로가기
DB/MySQL

MySQL, DeadLock..? (주의점: S-Lock이 전파된다.)

by seongju.lee 2023. 9. 19.

프로그래머스 데브코스 4기, 2차 프로젝트인 Bmart를 진행 하면서,  팀원의 이슈해결을 도운 내용이 있어, 신나게 가지고 왔다.

 

아래 글을 작성할 시점에 알게 된 내용을 팀원분이 맞닥뜨려서 해결을 도울 수 있었다.

https://lsj31404.tistory.com/84

 

데이터 동시성 제어의 목적과 유형(Lock과 트랜잭션 격리수준)

동시성 제어 데이터베이스는 공유를 목적으로 하기 때문에 가능한 많은 트랜잭션을 동시에 수행시켜야 한다. 하지만, 동시에 수행함으로써 같은 데이터를 공유한다면 데이터의 일관성이 훼손

lsj31404.tistory.com


이 글의 주제는 아래와 같다.
낙관적 락을 적용한 엔티티에 대해 동시성 테스트 도중 DeadLock이 발생한 것을 확인할 수 있었다.

왜 이런일이 발생했을까??

 


제대로 이유를 알아보기에 앞서 발생한 문제상황에 대해 공유하면 아래와 같다.

 

문제상황

(주문 아이템을 생성할 때, Item의 재고 수량과 관련하여 정합성을 맞추기 위해 Lock을 사용해야 하는 상황)

알아보기 앞서 Lock을 적용시킨 Item 도메인과 그와 연관된 OrderItem도메인의 관계는 아래와 같다.

  • 주문 아이템을 생성하기 위해 OrderItem을 생성하고, FK로 Item의 id값을 참조하는 형태이다.
  • 여러 스레드에서 동시에 같은 Item을 참조하는 OrderItem을 생성하는 과정에서 DeadLock이 발생했다.

 

DeadLock 발생 이유

현재 테스트 코드에서 DeadLock이 발생한 근본적인 이유는 MySQL을 사용했기 때문이다.

MySQL Docs

위 MySQL 공식문서에 가보면 아래와 같은 문장이 있다.

If a FOREIGN KEY constraint is defined on a table, any insert, update, or delete that requires the constraint condition to be checked sets shared record-level locks on the records that it looks at to check the constraint. InnoDB also sets these locks in the case where the constraint fails.

FK 제약조건을 가지고 있는 테이블에서 Insert, Update, Delete는 제약조건이 위반되는지 확인해야 하기 때문에, 참조되는 FK 레코드에 대해 s-lock을 건다.

즉, FK 제약조건이 포함된 레코드에서는 참조되는 레코드에 대해 s-lock이 걸리기 때문에 DeadLock이 발생할 수 있는 여지가 생기는 것이다.

 

 

해당 프로젝트에서 DeadLock이 발생한 이유

그럼 다시, 위 연관관계로 돌아와서 DeadLock이 걸린 이유에 대해 자세히 살펴보자.

주문아이템이 생성되는 메커니즘은 아래와 같다.

1. 주문아이템에 추가할 아이템(Item) 조회
2. 재고수량이 올바르면 주문아이템(OrderItem) 생성
3. 아이템 재고수량을 줄이기 위해 아이템(Item) 업데이트

그렇다면, 위 과정을 T1과 T2가 동시에 수행될 때 어떻게 DeadLock이 발생 가능한지 살펴보면 아래와 같다.

  • 1번은 조회이니깐 PASS
  • 2번에서 OrderItem을 생성할 때, FK로 걸려있는 1번 Item 레코드에 s-lock을 건다.
  • 3번에서 T1이 Item을 수정하기 위해 1번 Item에 x-lock을 시도하지만, T2가 s-lock을 걸고 있기 때문에 대기상태에 빠진다.
  • 동시에, 3번에서 T2가 Item을 수정하기 위해 s-lock을 소유한 채로 x-lock을 시도하지만 T1이 s-lock을 걸고 있기 때문에 대기상태에 빠진다.

 

처음에는 이 방식에 대해 의문을 가졌다.
왜냐하면 "T1이나 T2가 점유하고 있는 s-lock이 해제되면 -> 재고수량을 업데이트하면 되지 않나??"라는 생각이 들었다.

이런 의구심을 가지고 로그를 살펴보았다.

 

 

DeadLock 발생 로그

------------------------
LATEST DETECTED DEADLOCK
------------------------

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 964 page no 4 n bits 160 index PRIMARY of table `nayb_mart`.`item` trx id 75282 lock mode S locks rec but not gap
Record lock

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 964 page no 4 n bits 160 index PRIMARY of table `nayb_mart`.`item` trx id 75282 lock_mode X locks rec but not gap waiting
Record lock

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 964 page no 4 n bits 160 index PRIMARY of table `nayb_mart`.`item` trx id 75281 lock mode S locks rec but not gap
Record lock

** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 964 page no 4 n bits 160 index PRIMARY of table `nayb_mart`.`item` trx id 75281 lock_mode X locks rec but not gap waiting
Record lock
  • T1과 T2가 S-Lock을 보유했지만, X-Lock을 얻기 위해 대기하는 과정에서 DEADLOCK이 발생한 것으로 보인다.

이 로그를 보고 내가 작성한 저 동작방식에 의해서 데드락이 발생했다는 것을 짐작할 수 있었고,
그렇기에 내 짐작("T1이나 T2가 점유하고 있는 s-lock이 해제되면 -> 재고수량을 업데이트하면 되지 않나??")은 틀렸다고 볼 수 있었다.

 

그래서 생각이 든 것이 MySQL의 기본 격리수준이었다.

바로 공식문서를 찾아봤고, 아래 문서에서 한 문장이 가장 눈에 띄었다.

https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html

The default isolation level for InnoDB is REPEATABLE READ.

해당 포스팅 가장 위에 배치해 둔 글에서 작성했듯이 REPEATABLE READ 수준의 격리 수준에 따라 위 동작방식에 의해 데드락이 발생했다는 것을 짐작할 수 있었다.

'DB > MySQL' 카테고리의 다른 글

[MySQL 8.0] InnoDB : In-Memory(1) - 버퍼 풀  (0) 2024.02.04