Deadlock found when trying to get lock가 뜨면 두 트랜잭션이 서로가 쥔 락을 기다리며 영원히 막힌 상태입니다. InnoDB는 이걸 감지하고 한쪽을 강제 롤백해서 푸는데, 롤백당한 쿼리가 에러로 튀어나온 것입니다. 원인은 추측하지 말고 로그로 봅니다.
먼저 직전 데드락을 본다
SQL
SHOW ENGINE INNODB STATUS\G
출력의 LATEST DETECTED DEADLOCK 섹션이 핵심입니다. 두 트랜잭션(*** (1) TRANSACTION, *** (2) TRANSACTION)이 각각 무슨 쿼리를 실행 중이었고, WAITING FOR THIS LOCK / HOLDS THE LOCK이 어떤 인덱스의 어떤 레코드인지 찍힙니다.
OUTPUT
*** (1) TRANSACTION:
UPDATE accounts SET balance = balance - 100 WHERE id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS index PRIMARY of table `app`.`accounts`
*** (2) TRANSACTION:
UPDATE accounts SET balance = balance - 50 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
*** WE ROLL BACK TRANSACTION (1)
위 예시는 (1)은 id=1을 잡고 id=2를, (2)는 id=2를 잡고 id=1을 기다린 전형적인 교차 락입니다.
원인별 대처
| 원인 | 신호 | 대처 |
|---|---|---|
| 락 획득 순서가 트랜잭션마다 다름 | 두 쿼리가 같은 행들을 반대 순서로 UPDATE | 항상 같은 순서(예: id 오름차순)로 접근 |
| 인덱스가 없어 범위 락이 넓음 | WHERE에 인덱스 없는 컬럼, 풀스캔 락 | 조건 컬럼에 인덱스 추가해 락 범위 축소 |
| 트랜잭션이 너무 길다 | 한 트랜잭션이 여러 테이블을 오래 쥠 | 트랜잭션을 짧게, 사용자 입력 대기 중 락 금지 |
| 갭 락 충돌 | INSERT 다발 + 범위 조건 | 격리 수준을 READ COMMITTED로 낮추는 것 검토 |
가장 흔한 건 1번입니다. 애플리케이션 두 곳에서 A→B, B→A 순으로 잠그면 타이밍이 겹칠 때 반드시 막힙니다.
점검 체크리스트
SQL
SHOW ENGINE INNODB STATUS\G -- 직전 데드락 두 트랜잭션 확인
SELECT * FROM performance_schema.data_locks; -- 현재 잡힌 락
SELECT * FROM performance_schema.data_lock_waits; -- 대기 중인 락 관계
EXPLAIN <문제 UPDATE>; -- 인덱스 안 타면 락이 넓어짐
데드락은 완전히 없앨 수는 없으므로, 애플리케이션은 데드락 에러를 잡아 재시도하도록 만드는 것이 정석입니다.
트랜잭션 격리 수준과 락 동작을 직접 두 세션으로 재현해보는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 할 수 있습니다.