
MySQL 심화: InnoDB Deadlock 메커니즘과 재시도 전략
1) 버전 기준
- MySQL 기준 버전: 8.4 Reference Manual
- 공식 문서 기준: InnoDB Deadlocks / Deadlock Detection / Handling
2) 핵심 개념
MySQL 공식 매뉴얼은 deadlock을 트랜잭션들이 서로 잠금을 기다려 진행할 수 없는 상태로 정의합니다. InnoDB는 deadlock을 감지하면 트랜잭션 하나를 롤백해 교착 상태를 해소합니다. 따라서 애플리케이션은 deadlock 롤백을 전제로 재시도 로직을 가져야 합니다.
- InnoDB deadlock 감지 후 victim 트랜잭션 롤백
- 애플리케이션 측 재시도(backoff 포함) 권장
- 관측: InnoDB status,
innodb_print_all_deadlocks옵션
3) 트레이드오프
- 동시성 극대화: 처리량은 높지만 잠금 경합과 deadlock 위험 증가
- 접근 순서 통일/락 범위 축소: deadlock 감소 효과가 있으나 구현 복잡도 증가
- 로그 수집 강화: 원인 분석은 쉬우나 로그량/보관 비용 증가
4) 장애 재현-해결
재현 시나리오
- 세션 A가 row 1을 잠금 후 row 2 접근 시도
- 세션 B가 row 2를 잠금 후 row 1 접근 시도
- 상호 대기로 deadlock 발생
- InnoDB가 한 트랜잭션을 롤백
해결 절차
- 모든 경로에서 레코드 접근 순서를 통일합니다.
- 트랜잭션을 짧게 유지하고 잠금 범위를 최소화합니다.
- deadlock 오류를 애플리케이션에서 포착해 재시도합니다.
- 필요 시 deadlock 상세 로그를 활성화해 패턴을 수집합니다.
5) 체크리스트
- [ ] deadlock 재시도 로직(멱등성 포함)이 구현되어 있는가?
- [ ] 엔터티 접근 순서가 서비스 전반에서 일관적인가?
- [ ] 장시간 트랜잭션이 잠금 경합을 만들지 않는가?
- [ ] deadlock 관측 절차(로그/상태조회)가 런북에 있는가?
- [ ] deadlock 로그량과 보관 정책이 합의되어 있는가?
실전 재시도 코드 예시 (Java/Spring)
Spring 환경에서 deadlock 재시도를 구현하는 실전 패턴입니다. @Retryable 어노테이션 또는 수동 재시도 루프를 사용할 수 있습니다.
@Service
public class OrderService {
private static final int MAX_RETRIES = 3;
@Transactional
public void processOrder(Long orderId) {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
// 엔터티 접근 순서를 항상 ID 오름차순으로 통일
updateInventory(orderId);
updatePayment(orderId);
return;
} catch (DeadlockLoserDataAccessException e) {
if (attempt == MAX_RETRIES) throw e;
try { Thread.sleep(50 * attempt); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
}
}
}
}
핵심 포인트: 재시도 시 지수 백오프(exponential backoff)를 적용하면 동시 트랜잭션 간 충돌 확률이 줄어듭니다. 또한 재시도 로직은 반드시 트랜잭션 경계 바깥에 위치해야 합니다. 트랜잭션 내부에서 재시도하면 이미 롤백된 트랜잭션 위에서 작업하게 되어 데이터 불일치가 발생할 수 있습니다.
6) 공식 링크
- Deadlocks in InnoDB: https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks.html
- Deadlock Detection: https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlock-detection.html
- How to Minimize and Handle Deadlocks: https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks-handling.html
7) 실전: Deadlock 재시도 구현 (Spring + NestJS)
InnoDB 데드락은 동시 트랜잭션 환경에서 피할 수 없습니다. 핵심은 예방보다 감지 후 재시도입니다. MySQL은 데드락을 감지하면 비용이 적은 트랜잭션을 롤백하므로, 애플리케이션에서 자동 재시도 로직을 구현해야 합니다.
Spring Boot — @Retryable 활용
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.annotation.Backoff;
import org.springframework.dao.DeadlockLoserDataAccessException;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Retryable(
retryFor = DeadlockLoserDataAccessException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2, random = true)
)
@Transactional
public void placeOrder(OrderRequest request) {
// 재고 차감 → 주문 생성 → 결제 처리
inventoryRepository.decreaseStock(request.getProductId(), request.getQuantity());
Order order = Order.create(request);
orderRepository.save(order);
paymentService.process(order);
}
}
NestJS + TypeORM — 수동 재시도
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, QueryFailedError } from 'typeorm';
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
constructor(private dataSource: DataSource) {}
async placeOrder(dto: OrderDto, retries = 3): Promise<Order> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await this.dataSource.transaction(async (manager) => {
await manager.query(
'UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?',
[dto.quantity, dto.productId, dto.quantity],
);
const order = manager.create(Order, dto);
return manager.save(order);
});
} catch (error) {
if (
error instanceof QueryFailedError &&
(error as any).errno === 1213 && // ER_LOCK_DEADLOCK
attempt < retries
) {
const delay = Math.min(100 * Math.pow(2, attempt), 2000);
this.logger.warn(`Deadlock detected, retry ${attempt}/${retries} after ${delay}ms`);
await new Promise((r) => setTimeout(r, delay));
continue;
}
throw error;
}
}
}
}
8) 데드락 발생 줄이기: 인덱스와 접근 순서
- 일관된 테이블/행 접근 순서: 트랜잭션 A가 inventory → order 순서면, 트랜잭션 B도 동일 순서를 유지합니다.
- 인덱스 최적화: WHERE 절에 인덱스가 없으면 테이블 풀 스캔 → 갭 락 확대 → 데드락 확률 증가.
- 트랜잭션 범위 최소화: 외부 API 호출을 트랜잭션 밖으로 빼세요.
-- 데드락 로그 확인
SHOW ENGINE INNODB STATUSG
-- 최근 데드락 정보 조회 (MySQL 8.0+)
SELECT * FROM performance_schema.data_locks
WHERE LOCK_STATUS = 'WAITING';
-- 갭 락 최소화를 위한 인덱스 추가
ALTER TABLE inventory ADD INDEX idx_product_id (product_id);
9) 관련 글
- MySQL InnoDB Buffer Pool 튜닝 — Buffer Pool 설정이 락 경합에 미치는 영향을 이해하세요.
- NestJS + TypeORM 트랜잭션 가이드 — QueryRunner를 사용한 수동 트랜잭션 관리와 데드락 재시도를 함께 다룹니다.
- Spring @Transactional 심화 — Isolation Level 설정이 데드락 빈도에 미치는 영향을 분석합니다.