본문 바로가기

데이터베이스

[transaction 동시성 문제④] 데드락 - 1

이 시리즈는 데이터베이스 동시성 문제를 겪은 경험과 그 해결책에 대한 내용이 담겨 있습니다.

 

이전 해결방법-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번 사물함 대여 시도
  • SELECT active_lent_count FROM cabinet
                   WHERE cabinet_id = N;
  • INSERT INTO
                  lent_history(                     
                       lent_history_id, cabinet_id, user_id              
                  )              
                  VALUES (3, N, 3);
  • UPDATE CABINET
                 SET status = FULL, active_lent_count = 3,
                         version = 3 + 1
                 WHERE cabinet_id = N and version = 3
사용자 B(userId=4)가 N번 사물함 대여 시도
  • SELECT active_lent_count FROM cabinet
                   WHERE cabinet_id = N;
  • INSERT INTO
                  lent_history(                     
                       lent_history_id, cabinet_id, user_id              
                  )              
                  VALUES (3, N, 4);
  • UPDATE CABINET
                 SET status = FULL, active_lent_count = 3,
                         version = 3 + 1
                 WHERE cabinet_id = N and version = 3

(lock을 잡는 시나리오는 아래와 같습니다.)

select: cabinet slock (acquire) select: cabinet slock (acquire)
insert: lent_history xlock (acquire) insert: lent_history xlock (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에서 데드락이 발생한 진짜 이유에 대해 다루겠습니다...