/개발/MySQL ERROR 1205 Lock wait timeout 진단: 락 대기 vs 데드락 구분
개발MySQL ERROR 1205InnoDB 락 경합

MySQL ERROR 1205 Lock wait timeout 진단: 락 대기 vs 데드락 구분

MySQL ERROR 1205(Lock wait timeout)와 1213(데드락)을 SQLSTATE로 구분하고, performance_schema.data_locks·INNODB_TRX·SHOW ENGINE INNODB STATUS로 블로킹 트랜잭션을 5분 안에 찾는 복붙 SQL을 정리했습니다.

MySQL ERROR 1205 Lock wait timeout 진단: 락 대기 vs 데드락 구분

MySQL ERROR 1205 Lock wait timeout 실전 진단: 락 대기 vs 데드락 구분법

운영 중인 서비스에서 갑자기 트랜잭션이 멈추고 다음 메시지가 떴다면, 이 글은 정확히 당신을 위한 것입니다.

CODE
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

메시지 끝에 try restarting transaction이 붙어 있다고 해서 진짜로 재시도만 반복하면, 근본 원인은 그대로 남아 같은 장애가 끝없이 재발합니다. 그리고 한 가지 더 — 예전에 다룬 'PostgreSQL too many clients'(커넥션 풀 고갈, 즉 연결 자원이 부족한 문제)와 이 글은 각도가 완전히 다릅니다. 여기서는 연결은 멀쩡히 살아있는데 트랜잭션들이 서로의 락을 기다리느라 멈춰버린 상황을 다룹니다.

1205와 1213은 전혀 다른 문제다

가장 먼저 할 일은 두 에러 문자열을 나란히 놓고 구분하는 것입니다.

CODE
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
구분ERROR 1205 (락 대기)ERROR 1213 (데드락)
SQLSTATEHY00040001
발생 메커니즘innodb_lock_wait_timeout(기본 50초)이 지나도록 락을 못 얻음InnoDB가 순환 대기를 즉시 감지
처리 방식대기하던 한 트랜잭션만 타임아웃 실패InnoDB가 비용이 적은 한쪽을 victim으로 즉시 롤백
발생까지 시간타임아웃(수십 초)을 기다림즉시(밀리초)
핵심 신호"느리게 실패한다""한쪽이 바로 죽는다"

핵심은 이렇습니다. 1205는 누군가 락을 너무 오래 잡고 안 놔줘서 발생하는 "단방향 대기"이고, 1213은 두 트랜잭션이 서로의 락을 마주 잡아 순환이 생긴 "양방향 교착"입니다. 1205가 떴다면 "누가 락을 오래 쥐고 있나"를 추적하고, 1213이 떴다면 "어떤 두 쿼리가 잠금 순서를 충돌시켰나"를 봐야 합니다.

직접 재현해 보기 (Session A / B)

개념을 머리로만 이해하면 현장에서 헷갈립니다. 두 세션을 열어 직접 만들어 봅시다.

SQL
-- 준비
CREATE TABLE acct (id INT PRIMARY KEY, balance INT);
INSERT INTO acct VALUES (1,100),(2,100);

(A) 락 대기 → 1205 재현

SQL
-- Session A
BEGIN;
UPDATE acct SET balance = balance - 10 WHERE id = 1;  -- id=1 락 보유, 커밋 안 함

-- Session B (A가 안 놔주면 50초 후 실패)
BEGIN;
UPDATE acct SET balance = balance + 10 WHERE id = 1;
-- ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

(B) 데드락 → 1213 재현

SQL
-- Session A
BEGIN;
UPDATE acct SET balance = balance - 10 WHERE id = 1;  -- id=1 잠금

-- Session B
BEGIN;
UPDATE acct SET balance = balance - 10 WHERE id = 2;  -- id=2 잠금

-- Session A: 이제 id=2를 원함 (대기)
UPDATE acct SET balance = balance + 10 WHERE id = 2;

-- Session B: 이제 id=1을 원함 → 순환 발생!
UPDATE acct SET balance = balance + 10 WHERE id = 1;
-- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

순서만 다를 뿐인데 결과가 1205와 1213으로 갈립니다. 잠금 순서를 양쪽 세션이 똑같이(항상 id 작은 것부터) 맞추면 데드락은 사라집니다. 이게 뒤에 나올 재발 방지의 핵심입니다.

5분 진단: 블로킹 트랜잭션 추적 3종 세트

MySQL 8.0부터는 구식 INNODB_LOCKS가 제거되고 performance_schema.data_locks / data_lock_waits가 표준입니다. 아래 조인 쿼리 하나면 "누가 누구를 막고 있는지"가 한 줄로 보입니다.

SQL
-- ① 블로킹 ↔ 대기 트랜잭션을 한 번에
SELECT
  w.blocking_trx_id      AS blocking_trx,
  bt.trx_mysql_thread_id AS blocking_thread,
  bt.trx_query           AS blocking_query,
  w.requesting_trx_id    AS waiting_trx,
  wt.trx_mysql_thread_id AS waiting_thread,
  wt.trx_query           AS waiting_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx bt
  ON bt.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx wt
  ON wt.trx_id = w.requesting_trx_id;
SQL
-- ② 가장 오래된 active 트랜잭션 (장기 미커밋 범인 찾기)
SELECT trx_id, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_sec,
       trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;
SQL
-- ③ 어떤 락을 쥐고 있나
SELECT object_name, lock_type, lock_mode, lock_status, lock_data
FROM performance_schema.data_locks;

그리고 데드락의 "부검 보고서"는 항상 여기에 남습니다.

SQL
SHOW ENGINE INNODB STATUS\G

출력의 LATEST DETECTED DEADLOCK 블록 예시:

CODE
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-06-13 10:42:11 0x7f...
*** (1) TRANSACTION:
TRANSACTION 4211, ACTIVE 6 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136
UPDATE acct SET balance = balance + 10 WHERE id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `test`.`acct` ... id=1
*** (2) TRANSACTION:
TRANSACTION 4212, ACTIVE 4 sec
UPDATE acct SET balance = balance + 10 WHERE id = 1
*** WE ROLL BACK TRANSACTION (2)

HOLDS THE LOCK(S)(보유 락)와 WAITING FOR THIS LOCK(대기 락)을 양쪽 트랜잭션에서 읽으면 잠금 순서 충돌이 그대로 드러납니다. 단, SHOW ENGINE INNODB STATUS가장 최근 1건만 보여주므로, RDS/Aurora나 운영 서버라면 innodb_print_all_deadlocks = ON을 켜서 모든 데드락을 에러 로그에 남기는 걸 권장합니다.

원인 3패턴과 즉시 조치

실무에서 1205/1213의 90%는 아래 셋 중 하나입니다.

  1. 장기 미커밋 트랜잭션: 애플리케이션이 BEGIN 후 외부 API 호출 등으로 커밋이 지연된 경우. 위 ②번 쿼리에서 run_sec가 비정상적으로 큰 트랜잭션이 범인입니다.
  2. 인덱스 부재로 인한 갭락·넥스트키락 확대: 인덱스 없는 컬럼을 WHERE로 걸면 풀스캔하며 광범위한 락을 잡습니다.
  3. 배치 잡의 잠금 순서 충돌: 두 잡이 행을 서로 반대 순서로 갱신 → 데드락.

특히 2번은 EXPLAIN으로 바로 확인됩니다.

SQL
-- 인덱스 없음: 풀스캔 → 많은 행 잠금
EXPLAIN SELECT * FROM acct WHERE balance = 100 FOR UPDATE;
-- type: ALL, rows: 전체  → 넥스트키락이 테이블 전체로 확대

ALTER TABLE acct ADD INDEX idx_balance (balance);

-- 인덱스 추가 후: type: ref, rows: 소수만 잠금
EXPLAIN SELECT * FROM acct WHERE balance = 100 FOR UPDATE;

인덱스를 추가하면 잠기는 행 수가 수만 행에서 수 행으로 줄어, 락 경합 자체가 사라지는 경우가 많습니다.

즉시 조치 — KILL 대상 선정: 위 ①번 쿼리에서 blocking_thread(= trx_mysql_thread_id)를 골라 끊습니다. 선정 기준은 "가장 오래 쥐고 있으면서 다수를 막고 있는" 트랜잭션입니다.

SQL
KILL 8821;   -- blocking_thread 값

타임아웃 조정의 트레이드오프:

SQL
-- 세션 단위 (권장: 특정 배치만)
SET innodb_lock_wait_timeout = 5;
-- 전역 (영향 범위 큼, 신중히)
SET GLOBAL innodb_lock_wait_timeout = 20;
설정키우면 (예: 120초)줄이면 (예: 5초)
장점일시적 경합을 견딤빠른 실패 → 빠른 재시도
단점장애를 은폐, 대기 스레드 누적정상 트랜잭션도 조기 실패

현장 경험 한마디: 타임아웃을 키워 급한 불을 끄고 싶은 유혹이 강하지만, 저는 반대로 짧게 줄이고 애플리케이션에 재시도 로직을 두는 쪽을 선호합니다. 타임아웃을 늘리면 그 시간만큼 대기 스레드가 쌓여 어느 순간 커넥션 풀까지 같이 무너집니다. "느린 실패"보다 "빠른 실패 + 명시적 재시도"가 운영 가시성 측면에서 훨씬 낫습니다.

참고로 innodb_deadlock_detect는 기본 ON이라 데드락을 즉시 감지하지만, 초고동시성 환경에서는 감지 비용 자체가 병목이 될 수 있습니다. 이 경우 감지를 끄고 innodb_lock_wait_timeout을 짧게 두어 타임아웃으로 처리하는 전략도 있습니다.

재발 방지 체크리스트

  • 트랜잭션은 짧게: BEGINCOMMIT 사이에 외부 API 호출·사용자 입력 대기를 절대 넣지 않기.
  • 인덱스로 락 범위 축소: WHERE/JOIN 조건 컬럼에 인덱스를 두어 갭락·넥스트키락 확대를 차단.
  • 접근 순서 일관화: 여러 행을 갱신할 때 항상 같은 정렬(예: PK 오름차순)로 접근해 순환 대기를 원천 차단.
  • 격리수준 재검토: 갭락이 부담되면 READ COMMITTED 검토(단, 복제·일관성 영향 확인).
  • 데드락 로깅 상시화: innodb_print_all_deadlocks = ON으로 모든 교착을 기록.

자주 묻는 질문 (FAQ)

Q. ERROR 1205가 떴는데 그냥 트랜잭션을 재시도하면 안 되나요? A. 재시도는 임시방편일 뿐입니다. 1205는 누군가 락을 오래 쥐고 있다는 신호이므로, information_schema.innodb_trx에서 장기 미커밋 트랜잭션을 찾아 근본 원인(긴 트랜잭션·인덱스 부재)을 제거해야 재발하지 않습니다.

Q. MySQL 8.0에서 INNODB_LOCKS 테이블이 안 보입니다. A. 8.0에서 제거되었습니다. 대신 performance_schema.data_locksdata_lock_waits를 사용하세요. 보유 락과 대기 관계를 더 정확하게 보여줍니다.

Q. 1205와 1213을 로그만 보고 어떻게 빨리 구분하나요? A. SQLSTATE로 구분하세요. HY000이면 락 대기(1205), 40001이면 데드락(1213)입니다. 데드락은 한쪽이 즉시 롤백되고 SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK에 기록이 남는 반면, 락 대기는 타임아웃 후 대기하던 쪽만 조용히 실패합니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.

작성 · Content Reviewer·검토 · 사람 편집자·발행 · 2026년 6월 12일

댓글

불러오는 중...