한 줄 요약: InnoDB 는 Wait-for Graph 에서 순환(cycle)을 감지하면 변경 row 수가 적은 작은 트랜잭션을 victim 으로 선택해 rollback 하며, SHOW ENGINE INNODB STATUSinnodb_print_all_deadlocksperformance_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 조인 쿼리
예방 1lock 범위 최소화EXPLAIN 으로 인덱스 확인, PK 기반 쿼리 튜닝
예방 2교착 구조 제거테이블·행 접근 순서 전 트랜잭션 동일하게 정렬
예방 3lock 보유 시간 단축트랜잭션 범위에서 외부 I/O·API 호출 제거
예방 4Gap 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_waitsinnodb_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 을 두 단계로 획득함.

  1. Secondary index 레코드에 lock 획득
  2. 해당 레코드가 가리키는 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 READREAD 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_formatROW 또는 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   |
+---------------+-------+

참고