데이터베이스 용어 사전
동시성 제어deadlock · 교착 상태 · 순환 대기

데드락

두 트랜잭션이 서로가 쥔 락을 기다려 영원히 멈추는 상태. DB가 자동 탐지해 한쪽을 롤백하므로 재시도가 정답이다.

두 트랜잭션이 서로가 쥔 락을 기다려 영원히 진행하지 못하는 상태.

전형적인 발생 — 락 순서가 엇갈릴 때

-- T1                                -- T2
UPDATE acct SET bal=bal-1            UPDATE acct SET bal=bal-1
  WHERE id='A';   -- A 잠금             WHERE id='B';   -- B 잠금
UPDATE acct SET bal=bal+1            UPDATE acct SET bal=bal+1
  WHERE id='B';   -- B 대기(T2 보유)     WHERE id='A';   -- A 대기(T1 보유)
T1 ── 보유 ──> A        T2 ── 보유 ──> B
T1 ── 대기 ──> B        T2 ── 대기 ──> A

       순환이 만들어졌다 = 데드락

DB는 자동으로 탐지한다

  • PostgreSQL — ERROR: deadlock detected
  • MySQL — ERROR 1213 (40001): Deadlock found when trying to get lock

둘 다 한쪽을 희생자로 골라 롤백시킨다. 즉 데드락은 시스템이 멈추는 사고가 아니라 한 트랜잭션이 실패하는 사건이다.

그래서 애플리케이션의 정답은 재시도다.

// 데드락은 예외 상황이 아니라 정상 흐름으로 다룬다
for (int attempt = 0; attempt < 3; attempt++) {
    try { return transfer(from, to, amount); }
    catch (DeadlockLoserDataAccessException e) { /* 짧게 대기 후 재시도 */ }
}

빈도를 낮추는 네 가지

  • ① 항상 같은 순서로 락을 잡는다
    • A → B 로 통일하면 순환 자체가 안 생긴다 (가장 효과가 크다)
  • ② 트랜잭션을 짧게 유지한다
    • 락을 쥐는 시간이 짧으면 겹칠 확률이 준다
  • ③ 필요한 행만 좁게 잠근다
  • ④ 적절한 인덱스를 만든다
    • 인덱스가 없으면 더 많은 행과 갭을 잠가 확률이 오른다

④가 자주 간과된다 — 인덱스는 조회 성능만이 아니라 락 범위를 결정한다.

진단

SHOW ENGINE INNODB STATUS\G     -- LATEST DETECTED DEADLOCK 섹션에 양쪽 SQL 이 남는다

PostgreSQL은 로그에 어떤 프로세스가 무엇을 기다렸는지 남는다. 어떤 두 문장이 엇갈렸는지를 보고 ①의 순서 통일을 적용하는 것이 표준 절차다.

면접 함정

  • "데드락은 없애야 하는 버그" → 완전히 없앨 수 없다. 자동 탐지 + 재시도로 다루고 빈도를 낮추는 문제다.
  • "데드락이 나면 DB가 멈춘다" → 한쪽만 롤백된다. 멈추는 건 락 대기가 길어지는 경우다.

실제 로그를 읽는 법

SHOW ENGINE INNODB STATUS\G

LATEST DETECTED DEADLOCK

*** (1) TRANSACTION: UPDATE acct SET bal=bal+1 WHERE id='B'

  • *** (1) HOLDS THE LOCK(S): — ... index PRIMARY of table acct ... lock_data: 'A' *** (1) WAITING FOR THIS LOCK: ... lock_data: 'B'

*** (2) TRANSACTION: UPDATE acct SET bal=bal+1 WHERE id='A' *** (2) HOLDS THE LOCK(S): ... lock_data: 'B' *** (2) WAITING FOR THIS LOCK: ... lock_data: 'A' *** WE ROLL BACK TRANSACTION (2)

읽는 순서는 HOLDSWAITING FOR 다. 양쪽의 이 두 줄만 보면 어떤 두 자원이 엇갈렸는지 즉시 나온다. 위 경우 A와 B의 순서가 반대이므로, 처방은 항상 id 오름차순으로 잠그도록 코드를 고치는 것이다.

-- PostgreSQL: 로그에 남는다
-- ERROR: deadlock detected
-- DETAIL: Process 123 waits for ShareLock on transaction 456; blocked by process 789.
-- HINT: See server log for query details.

순서를 코드로 강제하기

// 두 계좌를 옮길 때 항상 작은 id 부터 잠근다 → 순환이 구조적으로 불가능
String first  = from.compareTo(to) < 0 ? from : to;
String second = from.compareTo(to) < 0 ? to   : from;
lockAccount(first);
lockAccount(second);

재시도할 때 주의

같은 순서로 바로 재시도하면 다시 부딪힐 수 있다

  • 짧은 랜덤 지연(지터)을 넣는다
  • 재시도 횟수 상한을 둔다 (보통 3회)

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 동시성 제어 — 2PL·MVCC·락·데드락(PostgreSQL·MySQL)