분명히 다른 행을 INSERT했는데 데드락이 났거나, 존재하지도 않는 범위에서 Lock wait timeout이 떴다면 범인은 대부분 **갭 락(gap lock)**입니다. InnoDB는 기본 격리수준인 REPEATABLE READ에서 팬텀 읽기를 막기 위해 실제 행뿐 아니라 인덱스 행 사이의 빈 공간까지 잠급니다.
세 가지 잠금 구분
| 잠금 | 잠그는 대상 | 목적 |
|---|---|---|
| 레코드 락 | 인덱스 레코드 1건 | 그 행 변경 차단 |
| 갭 락 | 인덱스 행 사이의 빈 구간 | 그 구간에 INSERT 차단 |
| 넥스트키 락 | 레코드 락 + 직전 갭 | 팬텀 방지(기본 동작) |
예를 들어 id가 10, 20, 30인 테이블에서 WHERE id BETWEEN 15 AND 25 FOR UPDATE를 실행하면, 존재하는 20 한 건만이 아니라 10과 30 사이 구간 전체에 넥스트키 락이 걸립니다. 그래서 다른 트랜잭션이 id = 17을 INSERT하려 하면 대기에 걸립니다.
데드락이 생기는 흐름
가장 흔한 패턴은 두 트랜잭션이 같은 갭에 INSERT하려 할 때입니다.
-- 트랜잭션 A
SELECT * FROM orders WHERE user_id = 7 FOR UPDATE; -- user_id 7 없음 → 갭 락
-- 트랜잭션 B
SELECT * FROM orders WHERE user_id = 7 FOR UPDATE; -- 같은 갭에 공유 갭 락
-- A: INSERT ... user_id = 7 → B의 갭 락 때문에 대기
-- B: INSERT ... user_id = 7 → A의 갭 락 때문에 대기 → 데드락
갭 락은 서로 호환되기 때문에 둘 다 SELECT는 통과하지만, 이후 INSERT에서 교착됩니다.
진단과 완화
- 현재 잠금 확인 — 최신 MySQL은 성능 스키마로 본다.
SELECT * FROM performance_schema.data_locks;
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK 섹션
-
인덱스를 정확히 태운다 — 갭 락은 옵티마이저가 스캔한 범위에 비례한다. 조회 조건이 인덱스를 못 타면 더 넓은 구간이 잠긴다. 유니크 인덱스로 단건(
WHERE id = 20)을 조회하면 갭 락 없이 레코드 락만 걸린다. -
격리수준 조정 —
READ COMMITTED로 낮추면 넥스트키 락 대신 레코드 락만 사용해 갭 락이 사실상 사라진다. 단 팬텀 읽기는 허용되므로 트레이드오프를 이해하고 적용한다. -
트랜잭션을 짧게 — 잠금 구간을 좁히고, 여러 트랜잭션이 같은 순서로 행에 접근하도록 정렬해 교착 가능성을 줄인다.
요점 정리
- REPEATABLE READ의 기본 잠금은 넥스트키 락 = 레코드 락
+갭 락이다. - 없는 행을 조회해도 갭은 잠기므로, 같은 갭 INSERT 경쟁이 데드락의 단골 원인.
data_locks와SHOW ENGINE INNODB STATUS로 실제 잠금을 확인한다.- 단건 유니크 조회·
READ COMMITTED·짧은 트랜잭션으로 갭 락 영향을 줄인다.
갭 락을 직접 재현하고 data_locks 출력으로 잠금 범위를 확인하는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 할 수 있습니다.