이 시리즈는 데이터베이스 동시성 문제를 겪은 경험과 그 해결책에 대한 내용이 담겨 있습니다.
이전 해결방법-2 글에서는 해결방법-1 보다 발전된 낙관적 락 구현 방법을 얘기했습니다.
하지만 이 방법은 치명적인 단점인 데드락 발생 문제가 있습니다.
이번 글에서는 데드락이 왜 발생했는지 원인과 시나리오를 살펴보겠습니다.
먼저 데드락이 무엇인지 간단하게 살펴보겠습니다.
멀티쓰레드 환경에서 공유자원이 있을 때, 동시성 문제가 발생할 수 있습니다. 이 문제를 예방하기 위해 lock 기법을 도입할 수 있습니다.
공유자원에 접근하기 위해선 lock이라는 특수한 자원을 획득해야 하고, 공유자원에 대한 읽기, 수정 작업이 끝나면 lock을 내려 놓습니다.
이렇게 한 쓰레드가 lock을 획득하면, 이 쓰레드가 lock을 내려 놓을 때까지 나머지 쓰레드들은 대기해야 합니다.
화장실이 하나 밖에 없을 때, 한 명이 화장실을 독점으로 사용하고 나머지는 기다리는 예시를 떠올릴 수 있습니다.
이렇게 lock 기법에서는 특수한 자원인 lock에 대해 wait - acquire - release 순서를 거쳐야 합니다.
이때 특별한 목적으로 의도한 것이 아닌 이상, wait에서 acquire로 넘어가지 않는다면 그것은 문제입니다.
예를 들어, acquire 단계인 쓰레드가 무한 루프를 돌려서 다른 쓰레드들은 wait 상태에서 벗어나질 못 하는 상황을 생각할 수 있습니다.
wait에서 acquire로 넘어가지 않는 상황은 더 있습니다. 그 중 하나가 deadlock 입니다.
예를 들어, 특수한 자원인 lock이 lock-1, lock-2 이렇게 있습니다.
쓰레드는 lock-1과 lock-2 모두 acquire 한 뒤에 비로소 공유자원에 접근해야 한다고 합시다.
그럼 다음과 같은 시나리오가 나올 수 있습니다.
쓰레드 A acquire [lock-1] wait [lock-2] |
쓰레드 B acquire [lock-2] wait [lock-1] |
A의 입장에서, A는 상대방(B)이 소유한 lock을 기다립니다. 그와 동시에 A가 소유한 lock을 상대방(B)이 기다립니다.
이는 B의 입장에서도 마찬가지입니다.
이렇게 각자가 lock을 소유하고 있으면서, 서로 상대방의 lock을 기다리는 상황이 deadlock입니다.
이런 deadlock 상황은 데이터베이스에서도 발생합니다.
단 하나의 데이터베이스를 사용할 때, 이는 하나의 공유자원 입니다.
그리고 다수의 세션이 데이터베이스에 연결될 수 있습니다.
다수의 세션이 동시에 CRUD 작업을 하면 동시성 문제가 발생할 수 있기때문에,
일반적으로 트랜잭션을 통해 lock 기법을 사용해서 CRUD합니다.
다수의 세션이 lock 기법을 이용하여 공유자원에 접근 하는 것이니, 데드락의 발생 가능성을 충분히 의심할 수 있습니다.
그리고 실제로도 데드락이 발생하는 시나리오가 많이 있습니다.
데이터베이스에서 트랜잭션을 통해 lock 기법을 사용합니다. 이때 트랜잭션의 고립수준, slock과 xlock 등 다양한 요소가 존재합니다.
그래서 다양한 데드락 시나리오가 나올수 있습니다.
+ mysql 공식문서에 데드락 예시가 있습니다. 명령어, 키워드 얻기에 좋습니다.
이제 해결방법-2 에서 데드락이 어떻게 발생한건지 보겠습니다. 일단 다음 전제가 있습니다.
- mysql 공식문서에 따르면, innodb의 트랜잭션 디폴트 고립수준은 repeatable-read 입니다.
- slock과 xlock의 규칙은 아래와 같다고 하겠습니다.
- select 문을 사용하면 (row-level) slock이 걸립니다.
insert, update, delete 문을 사용하면 (row-level) xlock이 걸립니다.- slock이 걸려있으면 (다른 트랜잭션이 그 자원에) slock을 걸 수 있습니다.
- slock이 걸려있으면 (다른 트랜잭션이 그 자원에) xlock을 걸 수 없습니다.
- xlock이 걸려있으면 (다른 트랜잭션이 그 자원에) slock을 걸 수 없습니다.
cabinet 동시성 문제 1
전제: N번 공유사물함은 최대 3명이 빌릴 수 있습니다. 현재 2명이 대여 중입니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
1 | 1 | N | 2023-5-15 | ``` |
2 | 2 | N | 2023-5-16 | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | AVAILABLE | 3 | 3 | 2 | ``` |
상황: 2명의 유저가 동시에 N번 사물함을 대여 시도합니다.
예상 결과: 2명 중 1명은 대여 성공, 나머지 1명은 대여 실패해야 합니다.
(기존 비지니스 로직에서 sql 이 사용되는 부분만 보겠습니다.)
사용자 A(userId=3)가 N번 사물함 대여 시도
|
사용자 B(userId=4)가 N번 사물함 대여 시도
|
(lock을 잡는 시나리오는 아래와 같습니다.)
select: cabinet slock (acquire) | select: cabinet slock (acquire) |
update: cabinet xlock (wait) | update: cabinet xlock (wait) |
deadlock | deadlock |
왼쪽 세션 입장에서, update시 cabinet xlock을 획득하려고 합니다. 그런데 오른쪽 세션이 이미 cabinet slock을 획득한 상태입니다.
그래서 왼쪽 세션은 오른쪽 세션이 cabinet slock을 놓을 때 까지 기다려야 합니다.
이는 오른쪽 세션 입장에서도 마찬가지 입니다.
따라서 데드락입니다.
데이터베이스 데드락 시나리오에서 많이 볼 수 있는 slock 획득, xlock 대기 시나리오 였습니다.
사실 위 시나리오는 틀린 부분이 있습니다. (그래서 위 데드락 시나리오의 해결방법을 다루지 않았습니다)
mysql 공식문서에 따르면, 실제로 innodb는 (repeatable-read 고립수준에서) select 문 실행시 어떠한 lock도 걸지 않습니다.
위 시나리오의 select 실행에서 slock이 걸리는 것으로 적어놓았는데, 실제로는 lock 이 일절 걸리지 않았다는 것입니다.
"select 문에서 slock 획득, update 문에서 xlock 대기 시나리오"가 원인이 아니었던 것입니다.
(분명 책에서는 repeatable-read 고립수준에서 select 문 실행시 slock이 걸린다고 했는데,
아마도 그 내용은 옛날 버전의 mysql 기준으로 작성된 내용이었던 거 같습니다.)
하지만, 데드락은 실제로 분명 발생했습니다. 도대체 해결방법-2에서 발생한 데드락 문제의 원인은 무엇일까요?
답은 외래키 제약 때문입니다. 이에 대해선 다음 글에 이어서 다루겠습니다.
다음 글에서 이 해결방법-2에서 데드락이 발생한 진짜 이유에 대해 다루겠습니다...
'데이터베이스' 카테고리의 다른 글
[transaction 동시성 문제⑥] 테스트 (0) | 2023.08.07 |
---|---|
[transaction 동시성 문제⑤] 데드락 - 2 (0) | 2023.07.28 |
[transaction 동시성 문제③] 해결 - 2 (0) | 2023.07.13 |
[transaction 동시성 문제②] 해결 - 1 (0) | 2023.07.12 |
[transaction 동시성 문제①] 원인 (0) | 2023.07.11 |