Published on

MySQL InnoDB 데드락 1213 에러 원인·해결

Authors

서버에서 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 이 간헐적으로 튀어나오면, 대부분은 “특정 순간에만” 재현되고 “로그 없이는” 원인을 특정하기 어렵습니다. 하지만 InnoDB 데드락은 원리상 반드시 서로가 서로의 락을 기다리는 순환 대기가 있어야 하므로, 락의 종류와 쿼리 패턴을 이해하면 재발 빈도를 크게 줄일 수 있습니다.

이 글은 1213 에러의 핵심 원인, SHOW ENGINE INNODB STATUS 로 데드락을 읽는 방법, 그리고 인덱스/쿼리/트랜잭션 구조를 바꿔서 해결하는 방법을 실무 관점에서 정리합니다.

1213 데드락이 의미하는 것

  • 1213은 InnoDB가 데드락을 감지해 희생자 트랜잭션 하나를 강제 롤백했다는 뜻입니다.
  • 이는 “DB가 망가졌다”가 아니라, 동시성 상황에서 정상적으로 발생 가능한 이벤트입니다.
  • 중요한 포인트는 두 가지입니다.
    • 애플리케이션은 1213을 재시도 가능한 오류로 처리해야 합니다.
    • DB/쿼리 설계는 데드락이 자주 발생하지 않게 구조를 개선해야 합니다.

데드락 vs 락 대기 타임아웃

  • 데드락(1213): 순환 대기라서 InnoDB가 즉시 감지하고 한 쪽을 죽입니다.
  • 락 대기 타임아웃(1205): 순환 대기는 아니지만, 오래 기다리다 innodb_lock_wait_timeout 을 초과합니다.

둘 다 “경합”이 원인이지만, 1213은 보통 서로 다른 순서로 같은 리소스를 잠그는 패턴이 트리거입니다.

InnoDB에서 데드락이 자주 생기는 대표 패턴

1) 업데이트 순서가 다른 경우(가장 흔함)

두 트랜잭션이 같은 테이블의 서로 다른 행을 업데이트하되, 락을 잡는 순서가 반대면 데드락이 쉽게 납니다.

예: 계좌 이체(두 행 업데이트)에서 트랜잭션마다 계좌를 잡는 순서가 다를 때

-- 트랜잭션 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 트랜잭션 B
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

해결의 핵심은 “항상 같은 순서로 잠근다” 입니다. 예를 들어 id 오름차순으로 정렬해 처리하면 데드락 확률이 급감합니다.

2) 보조 인덱스 갱신 + 다른 조건 업데이트

InnoDB는 UPDATE 시 PK 레코드뿐 아니라 관련된 보조 인덱스 엔트리도 잠급니다. 두 트랜잭션이 서로 다른 조건으로 같은 행 집합을 건드리면, 보조 인덱스에서 락 경합이 꼬이면서 데드락이 발생할 수 있습니다.

특히 다음이 위험합니다.

  • 조건 컬럼에 적절한 인덱스가 없어 스캔 범위가 넓은 업데이트
  • 유니크 인덱스/세컨더리 인덱스가 여러 개인 테이블에서 동시 업데이트

3) 갭 락과 넥스트키 락(범위 조건)

REPEATABLE READ(MySQL 기본)에서는 팬텀을 막기 위해 범위 조건에 갭 락/넥스트키 락이 걸립니다. 이 때문에 “딱 한 행만 업데이트하려고 했는데” 실제로는 인접 범위까지 잠겨 데드락이 생기기도 합니다.

예:

-- 인덱스가 (user_id, created_at) 인 경우
START TRANSACTION;
SELECT * FROM orders
WHERE user_id = 10 AND created_at BETWEEN '2026-01-01' AND '2026-01-31'
FOR UPDATE;
-- 다른 트랜잭션이 같은 user_id로 2월 데이터 INSERT/UPDATE를 하다가
-- 갭 락에 걸려 꼬일 수 있음
COMMIT;

해결은 아래 중 조합입니다.

  • 범위 조건을 더 좁히고(가능하면 PK 기반)
  • 필요한 인덱스를 추가해 스캔 범위를 줄이고
  • 경우에 따라 격리수준을 READ COMMITTED 로 조정(부작용 검토 필수)

4) INSERT ... ON DUPLICATE KEY UPDATE 경합

동일한 유니크 키로 동시에 업서트를 하면, 유니크 인덱스 엔트리 잠금이 경합하면서 데드락이 발생할 수 있습니다. 특히 업서트가 여러 테이블을 건드리거나, 트리거가 있으면 더 복잡해집니다.

5) 외래키 제약으로 인한 잠금 전파

부모/자식 테이블에서 FK가 걸려 있으면, 자식 삽입/삭제 시 부모 레코드 존재 확인 등으로 잠금이 추가로 발생합니다. 트랜잭션이 부모와 자식을 다른 순서로 건드리면 데드락이 유발됩니다.

데드락 원인 파악: InnoDB 데드락 로그 읽기

가장 먼저 할 일은 데드락이 났을 때 아래를 즉시 실행해 마지막 데드락 정보를 확인하는 것입니다.

SHOW ENGINE INNODB STATUS\G

출력에서 LATEST DETECTED DEADLOCK 섹션을 찾습니다. 여기에는 보통 다음이 나옵니다.

  • 어떤 트랜잭션들이 충돌했는지
  • 각 트랜잭션이 실행한 SQL
  • 어떤 락을 잡고 있었고, 어떤 락을 기다렸는지
  • InnoDB가 어떤 트랜잭션을 롤백했는지

주의할 점:

  • 이 정보는 “마지막 1건”만 유지됩니다. 데드락이 자주 나는데 확인이 늦으면 덮어씌워집니다.
  • 운영에서는 MySQL 에러 로그로 데드락을 남기도록 설정하는 편이 안전합니다.

운영에서 데드락 로그를 남기는 설정

MySQL 8.0 기준으로는 다음 변수를 확인합니다.

SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
  • ON 으로 설정하면 데드락이 날 때마다 서버 로그에 기록됩니다.
  • 다만 로그가 많아질 수 있으니, 데드락이 잦은 기간에만 켜거나 수집 파이프라인을 준비하는 것이 좋습니다.

추적/재현이 어려운 문제를 로그로 좁혀가는 방식은 인프라 전반에서 공통적으로 유효합니다. 비슷한 접근으로 장애 원인을 좁히는 방법은 systemd 서비스가 계속 재시작될 때 원인 추적법 도 참고할 만합니다.

해결 전략 1: 트랜잭션 설계부터 줄이기

데드락을 “없애는” 것보다 “덜 나게” 만드는 게 현실적입니다. 가장 효과가 큰 순서대로 정리하면 다음과 같습니다.

1) 트랜잭션을 짧게

  • 트랜잭션 안에서 네트워크 호출, 파일 I/O, 외부 API 호출을 하지 않습니다.
  • 불필요한 SELECT ... FOR UPDATE 를 줄입니다.
  • 한번에 갱신하는 행 수를 줄이고, 배치라면 청크 단위로 커밋합니다.

2) 동일 리소스 잠금 순서를 강제

  • 여러 행/여러 테이블을 업데이트한다면, 항상 같은 순서로 접근합니다.
  • 예: accounts 는 항상 작은 id부터 잠그기

애플리케이션 레벨에서 다음처럼 정렬 후 업데이트하는 방식이 대표적입니다.

-- 예시: 두 계좌를 정렬해서 항상 동일 순서로 잠금
START TRANSACTION;
SELECT id FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- 이후 UPDATE 수행
COMMIT;

핵심은 FOR UPDATE 로 “잠금 순서를 먼저 고정”하는 것입니다.

3) 격리수준 조정 검토

REPEATABLE READ 에서 갭 락이 문제라면 READ COMMITTED 가 도움이 될 수 있습니다.

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 작업
COMMIT;

단, 격리수준 변경은 팬텀/일관성 요구사항에 영향을 주므로, 도메인 요구사항과 함께 검토해야 합니다.

해결 전략 2: 인덱스로 스캔 범위를 줄이기

데드락은 락의 “순서” 문제이기도 하지만, 락을 잡는 “범위”가 넓을수록 확률이 폭증합니다.

1) 업데이트/삭제 조건 컬럼에 인덱스가 있는지 확인

다음 같은 쿼리가 인덱스를 못 타면, InnoDB는 많은 레코드를 훑으며 락을 오래 잡을 수 있습니다.

UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 10 AND status = 'PENDING';

이 경우 (user_id, status) 복합 인덱스가 없으면 경합이 커집니다.

CREATE INDEX idx_orders_user_status ON orders(user_id, status);

2) EXPLAIN 으로 실제 접근 경로 확인

EXPLAIN UPDATE orders SET status = 'CANCELLED'
WHERE user_id = 10 AND status = 'PENDING';
  • typeALL 이면 풀스캔 가능성이 큽니다.
  • rows 추정치가 큰데 실제로도 큰 테이블이면, 데드락/타임아웃 모두 위험해집니다.

해결 전략 3: 쿼리 패턴을 데드락 친화적으로 바꾸기

1) 한 번에 많이 갱신하지 말고 청크 처리

예를 들어 대량 상태 변경 배치를 아래처럼 청크로 나눕니다.

-- 반복 실행(애플리케이션 루프)
START TRANSACTION;
SELECT id FROM orders
WHERE status = 'PENDING'
ORDER BY id
LIMIT 500
FOR UPDATE;

UPDATE orders SET status = 'EXPIRED'
WHERE id IN ( ...위에서 읽은 id 목록... );
COMMIT;
  • ORDER BY id 로 잠금 순서를 안정화
  • LIMIT 으로 락 보유 시간을 축소

2) 업서트 경합 완화

INSERT ... ON DUPLICATE KEY UPDATE 가 병목이면,

  • 가능하면 “업서트 대상 키”를 분산(샤딩 키/버킷 키)
  • 혹은 먼저 SELECT 로 존재 확인 후 업데이트(단, 이 또한 경쟁 조건이 있어 트랜잭션 설계 필요)

업서트가 본질적으로 “동일 키에 대한 경쟁”이라면, DB만으로 해결하기 어렵고 상위 레벨에서 재시도/큐잉/직렬화가 필요할 수 있습니다.

해결 전략 4: 애플리케이션에서 재시도는 필수

InnoDB는 데드락을 감지하면 한 트랜잭션을 희생자로 선택합니다. 즉, 애플리케이션은 다음을 기본으로 가져가야 합니다.

  • 1213을 잡아서 같은 트랜잭션을 재시도
  • 재시도는 즉시 연속 실행이 아니라 지수 백오프 + 지터
  • 재시도 횟수 제한 및 서킷 브레이커/경고

예: 의사코드(언어 중립)

for attempt in 1..maxRetries:
  begin
  try:
    do business queries
    commit
    return success
  catch err:
    rollback
    if err is deadlock(1213) or serialization_failure:
      sleep(backoff_with_jitter(attempt))
      continue
    else:
      throw
throw retry_exhausted

MSA 환경에서는 데드락 재시도와 함께 “부분 실패를 어떻게 복구할지”가 같이 따라옵니다. DB 트랜잭션 밖으로 작업이 퍼지는 구조라면 Saga 패턴의 재처리/보상 설계가 도움이 됩니다. 관련해서 MSA Saga 보상 트랜잭션 실패 재처리 설계 도 같이 읽어두면 설계 관점이 정리됩니다.

실전 체크리스트: 1213이 잦을 때 우선순위

1) 어떤 쿼리 조합인지 확보

  • SHOW ENGINE INNODB STATUS\G 의 데드락 섹션 수집
  • 가능하면 innodb_print_all_deadlocks 로 서버 로그에 남기기
  • 애플리케이션에서 1213 발생 시 쿼리 템플릿/바인딩 키/트랜잭션 경로를 구조화 로깅

2) 트랜잭션 범위 줄이기

  • 트랜잭션 내부에서 오래 걸리는 작업 제거
  • 큰 배치를 청크로 분할

3) 잠금 순서 통일

  • 다중 행/다중 테이블 업데이트 순서 고정
  • 필요하면 SELECT ... FOR UPDATE 로 순서 선점

4) 인덱스와 실행계획 점검

  • 업데이트/삭제 조건에 맞는 복합 인덱스 추가
  • EXPLAIN 으로 풀스캔 여부 확인

5) 격리수준/쿼리 형태 조정

  • 갭 락이 의심되면 READ COMMITTED 검토
  • 범위 조건을 PK 기반으로 재작성

자주 묻는 질문

Q1. 데드락이 나면 데이터가 꼬이나요?

아닙니다. InnoDB는 희생자 트랜잭션을 롤백하므로 원자성은 보장됩니다. 다만 애플리케이션 입장에서는 해당 요청이 실패하므로, 재시도나 사용자 경험 처리가 필요합니다.

Q2. innodb_lock_wait_timeout 을 늘리면 1213이 줄어드나요?

1213(데드락)에는 영향이 거의 없습니다. 타임아웃은 1205에 더 관련이 있습니다. 1213이 잦다면 “기다리는 시간”을 늘리기보다 “순환 대기 자체”를 줄여야 합니다.

Q3. 데드락을 완전히 없앨 수 있나요?

고동시성 환경에서 “완전 제거”는 어렵습니다. 목표는 다음입니다.

  • 데드락 확률을 낮추고
  • 발생 시 빠르게 복구되게(재시도)
  • 재시도 폭주를 막게(백오프)

결론

MySQL InnoDB의 1213 데드락은 대개 다음 세 가지가 겹칠 때 폭발합니다.

  • 트랜잭션이 길고
  • 락을 잡는 순서가 일관되지 않고
  • 인덱스 부재로 락 범위가 넓은 상태

따라서 해결도 같은 축으로 접근해야 합니다. 데드락 로그로 “어떤 쿼리 조합인지”를 먼저 고정하고, 트랜잭션을 짧게 만들고, 잠금 순서를 통일하고, 인덱스로 스캔 범위를 줄이세요. 그리고 마지막으로, 애플리케이션에서 1213은 반드시 재시도 가능한 오류로 표준화해두는 것이 운영 안정성의 핵심입니다.