한 줄 요약: InnoDB 는 Wait-for Graph 에서 순환(cycle)을 감지하면 변경 row 수가 적은 작은 트랜잭션을 victim 으로 선택해 rollback 하며,
SHOW ENGINE INNODB STATUS→innodb_print_all_deadlocks→performance_schema순으로 분석하고, 인덱스 최적화·접근 순서 정렬·짧은 트랜잭션이 1차 예방책이고, 격리 수준 변경은 애플리케이션 로직 영향까지 검토해야 하는 최후 수단임.
개요
Deadlock(교착 상태)은 두 트랜잭션이 서로 상대방의 lock 을 기다리며 진행이 영구히 멈추는 상태임. InnoDB 는 이를 자동 감지해 한쪽을 rollback 시키므로 서비스가 중단되지는 않지만, victim 트랜잭션의 작업이 유실되고 애플리케이션에 에러가 반환됨.
단순히 “lock 이 꼬인 상태”에서 분석을 멈추면 재발 방지가 어려움. 발생 원리 → 분석 도구 → InnoDB 특화 경합 유형 → 예방책 순으로 구조화해야 반복 장애를 막을 수 있음.
결론: DBA 대응 체크리스트
| 단계 | 확인 항목 | 도구 / 방법 |
|---|---|---|
| 1차 감지 | 최근 deadlock 1건 상세 로그 | SHOW ENGINE INNODB STATUS |
| 이력 수집 | 전체 deadlock 이벤트 누적 기록 | innodb_print_all_deadlocks = ON |
| 실시간 탐지 | 현재 lock 대기 세션 관계 조회 | performance_schema.data_lock_waits 조인 쿼리 |
| 예방 1 | lock 범위 최소화 | EXPLAIN 으로 인덱스 확인, PK 기반 쿼리 튜닝 |
| 예방 2 | 교착 구조 제거 | 테이블·행 접근 순서 전 트랜잭션 동일하게 정렬 |
| 예방 3 | lock 보유 시간 단축 | 트랜잭션 범위에서 외부 I/O·API 호출 제거 |
| 예방 4 | Gap Lock 범위 축소 | 격리 수준 READ COMMITTED 는 최후 수단으로 검토 |
Deadlock 발생 원리
Wait-for Graph 와 cycle 감지
InnoDB 는 lock 대기 관계를 내부적으로 Wait-for Graph 로 관리함. 트랜잭션 A 가 트랜잭션 B 가 점유한 자원을 기다리면 A → B 엣지가 생김. 이 그래프에서 순환(cycle)이 감지되는 순간 deadlock 으로 판정함.
A 가 B 를 기다리고 B 가 A 를 기다리는 순환이 형성되는 시점에 InnoDB 가 deadlock 으로 판정함.
Victim 선택 기준
순환이 감지되면 InnoDB 는 관여된 트랜잭션 중 변경 row 수가 적은 작은 트랜잭션을 victim 으로 선택해 강제 rollback 시킴. MySQL 8.4 문서는 트랜잭션 크기를 insert·update·delete 된 row 수로 판단한다고 설명함.
victim 트랜잭션은 ERROR 1213: Deadlock found when trying to get lock; try restarting transaction 에러를 받게 됨. 나머지 트랜잭션은 lock 을 획득해 정상 진행됨.
분석 도구 3종
1. SHOW ENGINE INNODB STATUS — 즉시 확인
가장 최근에 발생한 deadlock 1건의 상세 정보를 원시 텍스트로 출력함. 여러 번 발생하면 마지막 1건만 유지되므로, 장애 직후 즉시 실행하는 것이 중요함.
SHOW ENGINE INNODB STATUS\G------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-05-27 10:23:41 0x7f9a1c deadlock detected
*** (1) TRANSACTION:
TRANSACTION 7412, ACTIVE 0 sec starting index read
MySQL thread id 42, query id 1923 updating
UPDATE orders SET status = 'done' WHERE id = 100
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 31 page no 4 n bits 72
index PRIMARY of table `shop`.`orders`
lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 32 page no 3 n bits 72
index PRIMARY of table `shop`.`payments`
lock_mode X locks rec but not gap
*** (2) TRANSACTION:
TRANSACTION 7413, ACTIVE 0 sec starting index read
UPDATE payments SET confirmed = 1 WHERE order_id = 100
*** (2) HOLDS THE LOCK(S):
index PRIMARY of table `shop`.`payments` lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
index PRIMARY of table `shop`.`orders` lock_mode X locks rec but not gap
*** WE ROLL BACK TRANSACTION (2)위 출력은 대표 형식 예시임. 실제 환경에서는 SQL 내용·인덱스명·트랜잭션 번호가 다르게 나타남.
분석 시 확인할 3가지:
- SQL: 어느 쿼리가 관여했는지 — 애플리케이션 로직의 어느 경로인지 역추적함
- Index:
index PRIMARY인지 보조 인덱스인지 — lock 획득 경로를 파악함 - Lock 종류:
lock_mode X locks rec but not gap(record lock),lock_mode X locks gap before rec(gap lock), 별도 gap 제외 표시가 없는lock_mode X(next-key lock) 구분
2. innodb_print_all_deadlocks — 이력 누적
기본값 OFF. ON 으로 설정하면 발생하는 모든 deadlock 이벤트가 MySQL error log 에 누적 기록됨.
SET GLOBAL innodb_print_all_deadlocks = ON;SHOW ENGINE INNODB STATUS 는 최신 1건만 유지하므로, deadlock 이 빈번하게 발생하면 이전 이력이 덮어써짐. error log 에 누적해두면 발생 빈도·시간대 패턴을 파악할 수 있음.
error log 경로는 SHOW VARIABLES LIKE 'log_error' 로 확인함. SET GLOBAL 은 재시작 시 초기화되므로 영구 적용 시 my.cnf 에도 innodb_print_all_deadlocks = ON 을 추가해야 함.
3. performance_schema — 실시간 lock 대기 탐지
data_lock_waits 와 innodb_trx 를 조인하면 현재 lock 을 대기 중인 세션 간의 관계를 실시간으로 조회할 수 있음.
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM
performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r
ON r.trx_id = CAST(w.requesting_engine_transaction_id AS CHAR)
JOIN information_schema.innodb_trx b
ON b.trx_id = CAST(w.blocking_engine_transaction_id AS CHAR);-- 대기 중인 세션이 있을 때 출력 예시 (대표 형식)
*************************** 1. row ***************************
waiting_trx_id: 7414
waiting_thread: 52
waiting_query: UPDATE orders SET status = 'done' WHERE id = 100
blocking_trx_id: 7412
blocking_thread: 42
blocking_query: UPDATE payments SET confirmed = 1 WHERE order_id = 100각 행은 (대기 트랜잭션, 블로킹 트랜잭션) 쌍 하나를 나타냄. 대기 세션이 없으면 빈 결과셋이 반환됨.
deadlock 은 rollback 이 완료된 이후에는 performance_schema 에서 이력을 확인할 수 없음. deadlock 직전의 lock 체인이나 장시간 대기 세션을 포착하는 데 적합함.
InnoDB 특화 경합 유형
Gap Lock 경합 — INSERT 시 발생
InnoDB 는 REPEATABLE READ 격리 수준에서 phantom read 를 막기 위해 인덱스 레코드 사이의 빈 공간(gap)에도 lock 을 검. Gap Lock 끼리는 공유(shared) 되므로 두 트랜잭션이 동시에 같은 gap 을 점유할 수 있음.
이후 두 트랜잭션이 동시에 그 gap 에 INSERT 를 시도하면, 각자 exclusive lock 으로 승격을 기다리게 되어 deadlock 이 발생함.
sequenceDiagram participant A as 트랜잭션 A participant G as Gap (id 5~10 사이) participant B as 트랜잭션 B A->>G: Gap Lock 획득 (shared) B->>G: Gap Lock 획득 (shared) ← 동시 허용 A->>G: INSERT id=7 → X lock 승격 시도 B->>G: INSERT id=8 → X lock 승격 시도 Note over A,B: 서로 상대방의 Gap Lock 해제를 대기 → Deadlock
격리 수준을 READ COMMITTED 로 낮추면 locking read·UPDATE·DELETE 에서 일반적인 Gap Lock 사용이 사라져 이 유형의 range 경합이 크게 줄어듦. 다만 foreign key 제약 확인과 duplicate-key 확인에는 Gap Lock 이 여전히 사용될 수 있음.
READ COMMITTED 는 row-based binary logging 만 지원됨. binlog_format = MIXED 에서는 서버가 row-based logging 을 자동 사용하지만, STATEMENT 전제 운영 환경에서는 replication 정책을 먼저 점검해야 함.
Secondary Index 경합 — lock 획득 경로 교차
Secondary index 를 통한 UPDATE·DELETE 는 lock 을 두 단계로 획득함.
- Secondary index 레코드에 lock 획득
- 해당 레코드가 가리키는 clustered index(PK) 레코드에 lock 획득
서로 다른 Secondary index 에서 시작한 두 쿼리가 같은 PK 레코드를 향하면서 획득 순서가 교차하면 deadlock 이 발생함.
필터 조건에 맞는 복합 인덱스를 추가해 스캔·잠금 대상 row 수를 줄이거나, 갱신 대상 PK 목록을 먼저 확정한 뒤 PK 순서로 갱신하면 교차 경합을 줄일 수 있음.
예방책 4가지
1. 인덱스 최적화 — lock 범위 최소화
인덱스를 타지 못하는 쿼리는 full scan 으로 빠지며, 이 경우 검색 범위 전체에 lock 이 걸려 경합 가능성이 크게 높아짐.
EXPLAIN SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 9842 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+type: ALL 이면 테이블 전체 레코드에 lock 이 걸리는 상황임. 인덱스 추가 또는 PK 기반 쿼리로 재작성해야 함. PK 컬럼이 없는 테이블은 InnoDB 가 내부적으로 숨겨진 row ID 를 clustered index 로 쓰게 되어 lock 추적이 어려워짐 (generated-invisible-primary-keys 참조).
2. 자원 접근 순서 정렬 — 교착 구조 제거
여러 테이블·행을 수정하는 트랜잭션들이 접근 순서를 항상 동일하게 유지하면, 트랜잭션들은 직렬로 대기할 뿐 cycle 이 형성되지 않음.
Bad: 트랜잭션 A → orders(id=1) → payments(id=1)
트랜잭션 B → payments(id=1) → orders(id=1) ← 순서 불일치 → cycle 가능
Good: 트랜잭션 A → orders(id=1) → payments(id=1)
트랜잭션 B → orders(id=1) → payments(id=1) ← 순서 일치 → 직렬 대기애플리케이션 레이어에서 PK 오름차순(ORDER BY id ASC)으로 정렬 후 처리하는 방식이 구현상 가장 단순함.
3. 트랜잭션 범위 최소화 — lock 보유 시간 단축
트랜잭션 안에 외부 API 호출, 파일 I/O, 복잡한 비즈니스 계산을 포함하면 lock 보유 시간이 길어져 경합 가능성이 높아짐. 데이터 변경 SQL 만 트랜잭션 안에 두고 나머지는 트랜잭션 밖으로 꺼내는 것이 원칙임.
4. 격리 수준 조정 — 마지막에 검토할 선택지
REPEATABLE READ → READ COMMITTED 전환은 Gap Lock 이 원인인 deadlock 을 줄이는 강한 선택지지만, 운영 중 전체 격리 수준 변경은 마지막에 검토해야 함. 기존 애플리케이션 로직이 REPEATABLE READ의 트랜잭션 snapshot, phantom 방지, lock 범위에 기대고 있을 수 있으므로 동작 의미가 바뀔 수 있음.
따라서 먼저 인덱스·접근 순서·트랜잭션 범위를 조정하고, 그래도 Gap Lock 경합이 병목으로 남을 때 세션 단위나 특정 배치 트랜잭션 단위 변경부터 검토하는 편이 안전함. 모든 Gap Lock 이 사라지는 것도 아니고 foreign key 제약 확인·duplicate-key 확인 예외가 남음.
-- 다음 트랜잭션 1회만 변경
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- Gap Lock 경합이 큰 작업 실행
COMMIT;
-- 배치 전용 세션에서 이후 트랜잭션만 변경
SET SESSION transaction_isolation = 'READ-COMMITTED';서버 전체 GLOBAL 변경이나 my.cnf 영구 변경은 충분한 회귀 테스트와 장애 대응 계획이 있을 때만 검토해야 함.
전환 전 binlog_format 이 ROW 또는 MIXED 인지 확인 필요함. MySQL 8.4 기준 READ COMMITTED 는 row-based binary logging 만 지원되며, MIXED 에서는 row-based logging 으로 자동 전환됨.
SHOW VARIABLES LIKE 'binlog_format';+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+참고
- MySQL 8.4 — InnoDB Deadlock Detection — Wait-for Graph 동작 원리 및 victim 선택 기준
- MySQL 8.4 — How to Minimize and Handle Deadlocks — deadlock 분석·재시도·접근 순서 정렬·짧은 트랜잭션 권장 사항
- MySQL 8.4 — InnoDB Locking — Record Lock · Gap Lock · Next-Key Lock 상세 정의
- MySQL 8.4 —
innodb_print_all_deadlocks— 시스템 변수 정의 및 기본값 - MySQL 8.4 —
performance_schema.data_lock_waits— lock 대기 테이블 스키마 - MySQL 8.4 — Transaction Isolation Levels —
REPEATABLE READ·READ COMMITTED의 lock 범위와 row-based logging 조건