MySQL "Deadlock found" 에러 진단과 해결: INNODB STATUS 로그 분석 가이드
운영 로그에서 Deadlock found when trying to get lock; try restarting transaction 메시지를 발견하고 이 글에 들어오셨을 겁니다. 새벽에 알림이 울렸거나, 결제·주문 트랜잭션이 간헐적으로 실패하고 있겠죠. 일단 안심하세요. 데드락은 데이터가 깨지는 게 아니라 InnoDB가 교착 상태를 감지하고 한쪽을 일부러 롤백시킨 정상적인 안전장치입니다.
이 글은 격리 수준 이론을 길게 설명하지 않습니다. 에러 로그 → 원인 특정 → 해결 → 재시도 코드 순서로, 지금 당장 적용할 수 있는 것만 다룹니다.
데드락은 왜 생기는가: 순환 대기의 정체
데드락의 본질은 단순합니다. 두 트랜잭션이 서로가 가진 락을 기다리며 교착되는 상태입니다.
세션 A: row 1 락 보유 → row 2 락 대기
세션 B: row 2 락 보유 → row 1 락 대기
┌──────────┐ ┌──────────┐
│ 세션 A │ HOLDS │ row 1 │
│ │────────▶│ │
│ │◀────────│ │ WAITS
└──────────┘ └──────────┘
│ WAITS ▲ HOLDS
▼ │
┌──────────┐ ┌──────────┐
│ row 2 │◀────────│ 세션 B │
└──────────┘ HOLDS └──────────┘여기서 알아둘 InnoDB 락은 세 가지입니다.
- 레코드 락(Record Lock): 특정 인덱스 레코드에 거는 락
- 갭 락(Gap Lock): 레코드 사이의 "빈 공간"에 거는 락 (팬텀 방지)
- 넥스트키 락(Next-Key Lock): 레코드 락 + 갭 락 (REPEATABLE READ의 기본 동작)
핵심은 이겁니다. 인덱스가 없으면 락이 넓게 잡히고, 락이 넓으면 충돌 확률이 폭증합니다. 그리고 트랜잭션마다 행을 잠그는 순서가 다르면 위 다이어그램 같은 순환 대기가 발생합니다.
범인 잡기: SHOW ENGINE INNODB STATUS 읽는 법
데드락이 터졌다면 가장 먼저 칠 명령어는 이것입니다.
SHOW ENGINE INNODB STATUS\G출력에서 LATEST DETECTED DEADLOCK 섹션을 찾으세요. 실제 출력은 이렇게 생겼습니다.
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-06-20 02:13:44 0x7f8a
*** (1) TRANSACTION:
TRANSACTION 84211, ACTIVE 6 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
UPDATE orders SET status='PAID' WHERE id=2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 4 n bits 80 index PRIMARY of table `shop`.`orders`
trx id 84211 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 84210, ACTIVE 9 sec starting index read
UPDATE orders SET status='SHIP' WHERE id=1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 4 n bits 80 index PRIMARY of table `shop`.`orders`
trx id 84210 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 4 n bits 80 index PRIMARY of table `shop`.`orders`
trx id 84210 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)한 줄씩 해부해 봅시다.
| 키워드 | 의미 |
|---|---|
*** (1) TRANSACTION / *** (2) TRANSACTION | 교착된 두 트랜잭션 |
UPDATE orders ... WHERE id=2 | 각 트랜잭션이 마지막으로 실행한 SQL (범인 단서) |
WAITING FOR THIS LOCK | 그 트랜잭션이 얻으려고 대기 중인 락 |
HOLDS THE LOCK(S) | 이미 보유 중인 락 |
index PRIMARY of table shop.orders | 어느 테이블, 어느 인덱스에 걸린 락인지 |
lock_mode X | 배타 락(쓰기 락) |
WE ROLL BACK TRANSACTION (1) | InnoDB가 **희생(victim)**으로 선택해 롤백한 쪽 |
여기서 핵심 독해 포인트: (1)이 대기하는 락을 (2)가 들고 있고, (2)가 대기하는 락을 (1)이 들고 있으면 순환 대기가 성립합니다. 그리고 index PRIMARY라고 명시돼 있으니 PK 기준 행 락 충돌임을 알 수 있죠. 만약 여기에 인덱스 이름 대신 풀스캔 흔적이 보이면 4-1 시나리오를 의심해야 합니다.
MySQL 8.0이라면 실시간 락 상태를 더 정밀하게 볼 수 있습니다.
-- 현재 어떤 트랜잭션이 무슨 락을 들고/기다리는지
SELECT * FROM performance_schema.data_locks;
-- 누가 누구를 기다리는지 (대기 관계)
SELECT * FROM performance_schema.data_lock_waits;직접 재현해 보기: 두 세션으로 데드락 만들기
원리를 손으로 익히는 게 가장 빠릅니다. 터미널 두 개를 열어 따라 해보세요.
-- 사전 준비
CREATE TABLE orders (
id INT PRIMARY KEY,
status VARCHAR(20)
);
INSERT INTO orders VALUES (1,'NEW'), (2,'NEW');-- 세션 A
BEGIN;
UPDATE orders SET status='SHIP' WHERE id=1; -- id=1 락 획득
-- 세션 B
BEGIN;
UPDATE orders SET status='SHIP' WHERE id=2; -- id=2 락 획득
-- 세션 A (id=2를 추가로 잠그려 시도 → 대기)
UPDATE orders SET status='PAID' WHERE id=2;
-- 세션 B (id=1을 추가로 잠그려 시도 → 순환 발생!)
UPDATE orders SET status='PAID' WHERE id=1;
-- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction세션 A는 1→2 순서로, 세션 B는 2→1 순서로 락을 요청했습니다. 락 순서가 엇갈린 것이 데드락의 직접 원인입니다.
시나리오별 진짜 원인과 해결책
4-1. 인덱스 부재로 락 범위가 테이블 전체로 확대
가장 흔하면서 가장 놓치기 쉬운 케이스입니다. WHERE 조건 컬럼에 인덱스가 없으면 InnoDB는 스캔하는 모든 행에 락을 겁니다.
-- email에 인덱스가 없는 상태
EXPLAIN UPDATE users SET grade='VIP' WHERE email='[email protected]';
-- type: ALL → 풀 테이블 스캔. 모든 행에 락이 걸린다!해결은 단순합니다. 조건 컬럼에 인덱스를 추가하세요.
ALTER TABLE users ADD INDEX idx_email (email);
EXPLAIN UPDATE users SET grade='VIP' WHERE email='[email protected]';
-- type: ref → 해당 행만 락. 충돌 확률 급감저도 실제로 트래픽이 몰리는 정산 배치에서 데드락이 폭증했는데, 원인이 WHERE settled_at = ? 컬럼의 인덱스 누락이었던 적이 있습니다. 인덱스 하나 추가로 데드락이 거의 사라졌죠. 데드락이 보이면 EXPLAIN부터 돌리는 습관을 들이세요.
4-2. 트랜잭션 내 UPDATE 순서 불일치
위 재현 예제가 바로 이 경우입니다. 해결책은 모든 코드에서 락을 거는 순서를 통일하는 것입니다.
-- 나쁜 예: 코드마다 제각각
-- A 서비스: id 1 → 2
-- B 서비스: id 2 → 1
-- 좋은 예: 항상 PK 오름차순으로 처리
UPDATE orders SET ... WHERE id IN (1,2) ORDER BY id;여러 행을 갱신할 땐 애플리케이션 레벨에서도 ID를 정렬한 뒤 순차 처리하세요. "항상 작은 키부터" 같은 규칙 하나로 순환 대기가 원천 차단됩니다.
4-3. 외래키 제약으로 인한 부모-자식 락
자식 테이블에 INSERT/UPDATE 시 InnoDB는 부모 행에 공유 락(S)을 겁니다. 부모를 수정하는 트랜잭션과 엇갈리면 데드락이 발생합니다.
-- 부모를 먼저 잠그는 트랜잭션과 자식 INSERT가 충돌
-- 해결: 비즈니스 로직에서 "부모 → 자식" 접근 순서 일관화
-- 또는 외래키 인덱스 점검 (자식 FK 컬럼 인덱스 필수)
SELECT * FROM information_schema.STATISTICS
WHERE TABLE_NAME='order_items' AND COLUMN_NAME='order_id';다시는 새벽에 깨지 않기: 재시도 코드와 예방
바로 쓰는 재시도 로직 (Python)
데드락은 완전히 없앨 수 없습니다. error code 1213을 잡아 지수 백오프로 재시도하는 것이 정석입니다.
import time
import random
from sqlalchemy.exc import OperationalError
def run_with_retry(session, work, max_retries=3):
for attempt in range(max_retries):
try:
result = work(session)
session.commit()
return result
except OperationalError as e:
session.rollback()
# MySQL 데드락 에러코드 1213
if e.orig.args[0] == 1213 and attempt < max_retries - 1:
backoff = (2 ** attempt) * 0.1 + random.uniform(0, 0.05)
time.sleep(backoff) # 지수 백오프 + 지터
continue
raiseSpring 환경 (Java)
@Retryable(
retryFor = DeadlockLoserDataAccessException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2))
@Transactional
public void processOrder(Long orderId) {
// 데드락 발생 시 자동 재시도
}의사코드 핵심:
재시도 횟수만큼 { 시도 → 1213이면 롤백 후 대기 → 재시도, 마지막 실패면 throw }
관련 변수와 운영 권장값
| 변수 | 설명 | 권장값 |
|---|---|---|
innodb_deadlock_detect | 데드락 자동 감지 | ON (고동시성에선 OFF 후 timeout 의존 검토) |
innodb_lock_wait_timeout | 락 대기 타임아웃(초) | 기본 50 → 운영은 5~10 권장 |
innodb_print_all_deadlocks | 모든 데드락을 에러 로그에 기록 | ON (운영 필수) |
innodb_print_all_deadlocks=ON을 켜두면 모든 데드락이 에러 로그에 남아, SHOW STATUS의 "마지막 1건만" 한계를 극복할 수 있습니다.
클라우드 매니지드 DB 모니터링
AWS Aurora/RDS MySQL 8.0이라면 Performance Insights에서 락 대기를 시각적으로 추적하고, performance_schema.data_lock_waits 테이블로 실시간 대기 관계를 조회하세요. 고동시성 마이크로서비스 환경에선 데드락이 "버그"가 아니라 "상수"이므로, 재시도 로직을 기본 인프라로 깔아두는 것이 안전합니다.
예방 체크리스트
- 모든 코드에서 락 획득 순서 통일 (예: PK 오름차순)
- WHERE/JOIN 조건 컬럼, 외래키 컬럼에 인덱스 보장 (EXPLAIN 확인)
- 트랜잭션 범위 최소화 — 외부 API 호출/대기를 트랜잭션 안에 넣지 말 것
-
SELECT ... FOR UPDATE남용 주의, 꼭 필요한 행만 - 1213 에러 재시도 로직 적용
-
innodb_print_all_deadlocks=ON+ 모니터링 쿼리 주기 실행
자주 묻는 질문 (FAQ)
Q. 데드락이 났는데 데이터가 손상되나요? A. 아닙니다. InnoDB는 교착을 감지하면 한쪽 트랜잭션만 자동 롤백하므로 데이터 정합성은 유지됩니다. 롤백된 트랜잭션만 재시도하면 됩니다.
Q. 재시도하면 무한 루프에 빠지지 않나요? A. 최대 재시도 횟수(보통 3회)와 지수 백오프를 두면 됩니다. 그래도 계속 실패하면 락 순서나 인덱스 같은 근본 원인을 SHOW ENGINE INNODB STATUS로 점검해야 합니다.
Q. 마지막 데드락 1건만 보여서 분석이 어렵습니다.
A. innodb_print_all_deadlocks=ON을 설정하면 발생한 모든 데드락이 MySQL 에러 로그에 기록되어 패턴 분석이 가능합니다. RDS/Aurora는 파라미터 그룹에서 설정하세요.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...