운영 DB에서 데드락(deadlock) 로그를 읽다가, lock_mode X locks rec but not gap, lock mode S 같은 표현이 어떤 락을 가리키는지는 알아도 어떤 문장이 왜 그 락을 잡았는지는 바로 설명하기 어려웠습니다. 그래서 데드락 추적에 앞서 락부터 정리했습니다.
이 글은 그 정리 노트입니다. InnoDB가 행을 어떻게 잠그는지 따라가며, 다음 편에서 다룰 데드락의 배경을 먼저 정리합니다. (개념 설명은 MySQL 공식 문서의 InnoDB Locking을 기준으로 했고, px201226님의 "MySQL 트랜잭션 락" 글을 참고해 구성을 잡았습니다. 갭 락과 삽입 의도 락 부분은 당근 기술블로그의 "MySQL Gap Lock 다시보기"·"두 번째 이야기"에서 실사례를 빌렸습니다. MariaDB의 InnoDB도 같은 락 모델을 따릅니다.)
1. 두 개의 기본 락: 공유락(S)과 배타락(X)
InnoDB의 행 락은 두 종류에서 출발합니다.
- 공유락(Shared Lock, S): 읽기를 위한 락. 여러 트랜잭션이 같은 행에 동시에 S락을 쥘 수 있습니다. 단, 그 행을 바꾸려는 배타락은 막습니다.
SELECT ... FOR SHARE(과거LOCK IN SHARE MODE)로 명시적으로 걸 수 있습니다. - 배타락(Exclusive Lock, X): 쓰기를 위한 락. 한 트랜잭션만 쥘 수 있고, 다른 어떤 락(S든 X든)과도 공존하지 못합니다.
SELECT ... FOR UPDATE나UPDATE/DELETE가 이 락을 겁니다.
정리하면 S락끼리는 공존하고, X락은 단독으로만 잡힙니다. 이 규칙이 뒤에 나올 데드락의 절반을 설명합니다.
| 보유 ↓ \ 요청 → | S | X |
|---|---|---|
| S | ✓ 공존 | ✗ 대기 |
| X | ✗ 대기 | ✗ 대기 |
여기서 한 가지를 미리 짚어 둡니다. 이미 S락을 쥔 행을 같은 트랜잭션이 X락으로 올리는 것(락 승격, lock upgrade)은, 그 사이 다른 트랜잭션도 같은 행에 S락을 쥐고 있으면 성공하지 못합니다. S락은 공존하지만, X로의 승격은 다른 락이 모두 풀려 있어야 하기 때문입니다.
2. 행을 잠그기 전에 손드는 락: 의도락(Intention Lock)
InnoDB는 테이블 락과 행 락을 함께 씁니다(다중 세분성 잠금, multiple granularity locking). 그런데 누군가 테이블 전체에 락을 걸려 할 때, "이 테이블 안 어딘가의 행에 락이 걸려 있나?"를 행을 일일이 훑어 확인하면 느립니다. 그래서 행 락을 걸기 전에, 테이블 레벨에 "나 이 테이블의 어떤 행을 잠글 거야"라는 깃발을 먼저 꽂습니다. 이게 의도락(Intention Lock)입니다.
- IS(Intention Shared): 행에 S락을 걸 의도. (
SELECT ... FOR SHARE) - IX(Intention Exclusive): 행에 X락을 걸 의도. (
SELECT ... FOR UPDATE,UPDATE,INSERT등)
의도락끼리는 대부분 호환됩니다. 둘 다 "행 단위로 락을 걸 예정"이라는 표시일 뿐이고, 실제 충돌은 행 레벨에서 가려지기 때문입니다. 공식 문서의 호환성 매트릭스는 다음과 같습니다.
| X | IX | S | IS | |
|---|---|---|---|---|
| X | ✗ | ✗ | ✗ | ✗ |
| IX | ✗ | ✓ | ✗ | ✓ |
| S | ✗ | ✗ | ✓ | ✓ |
| IS | ✗ | ✓ | ✓ | ✓ |
표에서 보이듯, 테이블 레벨 X락만 모든 것을 막습니다. 평소 우리가 만나는 데드락은 의도락이 아니라 행 레벨의 S/X에서 벌어지니, 의도락은 "이런 계층이 있다" 정도로만 알아두면 충분합니다.
3. InnoDB가 행을 잠그는 세 가지 방식: 레코드 / 갭 / 넥스트키 락
InnoDB의 행 락은 사실 "행"이 아니라 인덱스 레코드를 잠급니다. 잠그는 범위에 따라 셋으로 나뉩니다.
| 락 | 잠그는 대상 | performance_schema 표기 |
|---|---|---|
| 레코드 락(Record Lock) | 인덱스 레코드 하나 | X,REC_NOT_GAP / S,REC_NOT_GAP |
| 갭 락(Gap Lock) | 레코드와 레코드 사이의 빈 구간 | X,GAP / S,GAP |
| 넥스트키 락(Next-Key Lock) | 레코드 + 그 앞 갭 | (기본 표기) |
갭 락은 값을 잠그는 게 아니라 그 구간에 새 행이 끼어드는 것(INSERT)을 막는 락입니다. 같은 조건으로 두 번 읽었을 때 없던 행이 생기는 팬텀 리드(phantom read)를 막기 위한 장치입니다. 넥스트키 락은 레코드 락과 갭 락을 합친 것으로, REPEATABLE READ에서 범위 조건을 다룰 때 기본으로 쓰입니다.
갭 락이 언제 붙는지는 인덱스 종류를 탑니다.
- 기본키·유니크 인덱스 + 동등 조건(=)으로 정확히 1건: 레코드 락만. 갭은 안 잠급니다.
- 존재하지 않는 값 조회, 범위 조건, 또는 보조 인덱스(secondary index): 레코드 락 + 갭 락이 함께 붙습니다. 특히 보조 인덱스 동등 조회는 거의 항상 갭까지 잠그기 때문에, 한 건만 건드린 것처럼 보여도 인접 구간의 INSERT가 막힐 수 있습니다.
여기에 갭과 짝을 이루는 락이 하나 더 있습니다. 삽입 의도 락(INSERT Intention Lock)입니다. INSERT가 어떤 갭에 행을 넣기 직전 그 갭에 거는 특수한 갭 락이며, 다음 성질이 중요합니다.
- 갭 락끼리는 서로 호환됩니다. 여러 트랜잭션이 같은 갭에 갭 락을 함께 쥘 수 있습니다.
- 하지만 갭 락과 삽입 의도 락은 충돌합니다. 누군가 갭을 잠가두면, 그 갭에 INSERT 하려는 트랜잭션은 대기합니다.
이 "갭 락은 공존하지만 삽입 의도 락과는 비호환"이라는 비대칭이 6절의 또 다른 데드락 패턴을 만듭니다.
참고: 2편의 데드락 로그에는
locks rec but not gap이 등장합니다. 갭은 잠그지 않고 레코드 하나만 잠근, 위 표의 "레코드 락"입니다. 즉 2편의 교착은 갭 락이 아니라 레코드 락 + S→X 승격이 원인이며, 지금 설명한 갭 락은 그것과 별개의 데드락 갈래입니다.
4. 격리 수준이 락을 바꾼다 — 기본은 REPEATABLE READ
같은 SQL이라도 트랜잭션 격리 수준(isolation level)에 따라 잡는 락이 달라집니다. MySQL·MariaDB의 기본값은 REPEATABLE READ(RR)입니다.
- 일반
SELECT는 락을 안 건다. RR에서 평범한 조회는 MVCC(다중 버전 동시성 제어)로 스냅샷을 읽기 때문에, 락 없이도 일관된 결과를 봅니다. - 하지만 쓰기는 다르다.
UPDATE,DELETE, 그리고 다른 테이블을 읽어 값을 넣는INSERT ... SELECT는 MVCC로 처리되지 않고 실제 락을 잡습니다. RR이라도 MVCC는 읽기 동시성을 위한 것이며, 쓰기에는 적용되지 않습니다.
READ COMMITTED(RC)로 낮추면 갭 락을 거의 쓰지 않아 잠금 범위가 좁아지지만, 일관성 보장도 함께 약해집니다. 이 글과 2편의 내용은 모두 기본값 RR을 전제로 합니다.
5. 무슨 문장이 어떤 락을 잡나
이번 노트의 핵심 표입니다. 기본 격리 수준(RR) 기준으로, 자주 쓰는 문장이 거는 락을 정리했습니다.
| 문장 | 거는 락 |
|---|---|
일반 SELECT |
없음 (MVCC 스냅샷 읽기) |
SELECT ... FOR SHARE |
읽은 행에 S락 (+ IS) |
SELECT ... FOR UPDATE |
읽은 행에 X락 (+ IX) |
UPDATE ... WHERE |
조건 범위에 넥스트키 락, 실제 수정 행에 X락 |
단순 INSERT |
삽입 행에 X락 |
INSERT ... SELECT |
읽어온 원본 행에 S락 + 삽입 행에 X락 |
INSERT ... ON DUPLICATE KEY UPDATE |
중복키 감지 시 먼저 S락 → 갱신하며 X락 |
마지막 두 줄이 2편 데드락의 두 축입니다.
INSERT ... SELECT(또는 INSERT의VALUES안 서브쿼리): 값을 읽어온 원본 행에 S락이 걸립니다. 읽기만 한 것처럼 보여도, 그 값으로 다른 곳에 쓰는 이상 일관성을 지키기 위해 InnoDB가 S락을 잡습니다.ON DUPLICATE KEY UPDATE: 중복키를 만나면 그 인덱스 레코드에 먼저 S락을 잡아 확인하고, 갱신을 진행하며 X락으로 올립니다. 여기에 락 승격(S→X)이 들어 있습니다.
6. 데드락이 만들어지는 과정
데드락은 둘 이상의 트랜잭션이 서로가 쥔 락을 맞물려 기다리는 순환 대기(circular wait)입니다. 실무에서 마주치는 형태는 크게 셋입니다.
패턴 A — 락 순서 역전. 가장 고전적인 형태입니다.
trx-1: id=1 X락 획득 → id=2 X락 대기
trx-2: id=2 X락 획득 → id=1 X락 대기
└──────── 서로 물림 → 교착 ────────┘두 트랜잭션이 같은 자원들을 반대 순서로 잠그면 발생합니다. 해결책은 모든 트랜잭션이 같은 순서로 잠그도록 맞추는 것입니다.
패턴 B — S→X 승격 경쟁. 1절에서 짚어 둔 락 승격이 여기서 문제가 됩니다.
trx-1, trx-2: 같은 행에 S락을 함께 획득 (S끼리는 공존 OK)
trx-1: 그 행을 X로 승격하려 함 → trx-2의 S락 때문에 대기
trx-2: 그 행을 X로 승격하려 함 → trx-1의 S락 때문에 대기
└──────── 둘 다 못 올라감 → 교착 ────────┘INSERT ... ON DUPLICATE KEY UPDATE처럼 S락을 먼저 잡고 X로 올리는 문장을 두 세션이 같은 키로 동시에 실행하면 이 패턴에 빠집니다.
패턴 C — 갭 락과 삽입 의도 락의 충돌. 3절의 비대칭("갭 락끼리는 공존, 삽입 의도 락과는 비호환")이 여기서 작용합니다.
세션1: SELECT ... FOR UPDATE (없는 id=2) → 갭 락 획득
세션2: SELECT ... FOR UPDATE (없는 id=2) → 갭 락 획득 (갭 락끼리는 공존 OK)
세션1: INSERT id=2 → 삽입 의도 락 필요, 세션2의 갭 락과 충돌 → 대기
세션2: INSERT id=2 → 삽입 의도 락 필요, 세션1의 갭 락과 충돌 → 대기
└──────── 서로 물림 → 교착 ────────┘존재하지 않는 값 조회·범위 조건·보조 인덱스가 갭 락을 만들고, 거기에 두 세션이 같은 갭으로 INSERT를 시도하면 교착이 됩니다. 큐(queue)처럼 쓰는 테이블이나 빈 테이블(갭이 테이블 전체로 넓어집니다)에서 특히 자주 나타납니다. 해결책은 격리 수준을 READ COMMITTED로 낮춰 갭 락을 없애거나, 범위 조회(BETWEEN)를 기본키 동등 조회(IN (...))로 바꾸거나, 선택도 높은 인덱스를 더해 잠금 범위를 좁히는 것입니다. (당근 기술블로그의 두 글이 이 패턴과, 페이지 끝의 supremum 갭까지 다룹니다 — 위 참고 링크.)
참고: 패턴 B와 C는 둘 다 공존하던 약한 락(S락 / 갭 락)을 더 강한 락(X락 / 삽입 의도 락)으로 올리는 시점에서 막힌다는 공통점이 있습니다. 여럿이 함께 쥐던 락을 동시에 독점하려는 순간이 교착의 전형적인 구도입니다.
교착이 감지되면 InnoDB는 가장 가벼운 트랜잭션 하나를 골라 롤백합니다. "가벼움"은 information_schema.INNODB_TRX의 trx_weight(변경·잠근 행 수 등을 반영한 값)로 판단합니다. 살아남은 쪽은 정상 진행하므로, 애플리케이션은 데드락(에러 코드 1213)을 만나면 재시도하도록 작성하는 것이 일반적입니다.
7. 마무리 — 2편으로
여기까지가 데드락 로그를 읽기 위한 최소한의 어휘입니다. 정리하면,
- S락은 공존하고 X락은 독점한다. 그리고 S→X 승격은 다른 S락이 남아 있으면 막힌다.
- 행 락은 인덱스 레코드를 잠그며, 레코드 / 갭 / 넥스트키로 범위가 나뉜다.
- 기본 격리 수준 RR에서 일반 SELECT는 락이 없지만, 쓰기와
INSERT ... SELECT는 락을 잡는다. INSERT ... SELECT는 원본 행에 S락,ON DUPLICATE KEY UPDATE는 중복키에 S락 → X락 승격.- 데드락은 락 순서 역전(패턴 A), S→X 승격 경쟁(패턴 B), 갭 락↔삽입 의도 락 충돌(패턴 C)에서 발생한다.
이 요소들이 실제로 어떻게 맞물리는지를, 다음 편에서 운영 DB에 기록된 데드락 로그로 추적합니다. 평범해 보이는 upsert 쿼리 하나가 위 패턴 B를 그대로 재현한 사례입니다. → 2편: INSERT 한 번에 데드락이?에서 이어가겠습니다.