본문 바로가기

기술 블로그

DELETE-INSERT 패턴에서 발생하는 InnoDB Deadlock 분석

배경

  • 최근 우리 서비스는 Sentry를 통해 데드락 알람을 자주 받고 있다
  • 처음에는 중복 인덱스를 발견해서 제거하는 작업을 진행했다
    • 그럼에도 불구하고 동일한 로직에서 데드락이 발생하는 중이다
    • 조금은 빈도가 감소했을 수 있는데, 데드락 모니터링과 수집 및 통계화를 다른 회사에서는 어떻게 하는지 리서치가 필요해 보인다
  • 아무튼 데드락을 분석해보니, 동일한 레코드의 DELETE-INSERT가 하나의 트랜잭션 내 수행되는 API에서 발생하였다
  • 명확한 원인을 파악하고 전략을 세우기 위해 분석을 시도하였다

 

InnoDB Deadlock 분석

 

InnoDB 엔진 내부 상태를 확인하는 방법

 

 

RDMBS 클라이언트에서 위 명령어를 입력:

 

SHOW ENGINE INNODB STATUS
  • 명령어는 InnoDB 엔진 내부의 상태를 확인 가능하며, 보통 Deadlock을 분석하는데 사용
  • 참고) 최근에 발생한 Deadlock만 기록된다는 점을 유의
  • RDMBS 클라이언트에서 위 명령어를 입력하면 LATEST DETECTED DEADLOCK 섹션을 분석할 수 있다
  • 아래 예시는 우리 프로덕트 환경에서 발생한 로그에서 중요한 정보들은 마스킹 처리하고 가져왔다

 

------------------------  
LATEST DETECTED DEADLOCK  
------------------------  
2025-04-17 09:24:30  
*** (1) TRANSACTION:  
TRANSACTION 10539257, ACTIVE 0 sec starting index read  
mysql tables in use 3, locked 3  
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)  
MySQL thread id 958294, OS thread handle MASKED_HANDLE, query id MASKED_QUERY_ID xxx.xxx.xxx.xxx sample_db executing  
DELETE FROM sample_table WHERE (조건 생략)

*** (1) HOLDS THE LOCK(S):  
RECORD LOCKS space id MASKED_SPACE page no MASKED_PAGE n bits MASKED_INDEX index `sample_index` of table `sample_db`.`sample_table` trx id 10539257 lock_mode X

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:  
RECORD LOCKS space id MASKED_SPACE page no MASKED_PAGE index `sample_index` of table `sample_db`.`sample_table` lock_mode X waiting

*** (2) TRANSACTION:  
TRANSACTION 10539266, ACTIVE 0 sec inserting  
mysql tables in use 1, locked 1  
LOCK WAIT 17 lock struct(s), heap size 1128, 13 row lock(s)  
MySQL thread id 958304, OS thread handle MASKED_HANDLE, query id MASKED_QUERY_ID xxx.xxx.xxx.xxx sample_db update  
INSERT INTO sample_table (컬럼 생략) VALUES (데이터 일부 마스킹)

*** (2) HOLDS THE LOCK(S):  
(생략 가능하거나 중요 키만 남김)

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:  
RECORD LOCKS space id MASKED_SPACE page no MASKED_PAGE index `sample_index` lock_mode X locks gap before rec insert intention waiting

*** WE ROLL BACK TRANSACTION (1)

 

분석 방법:

  1. TRANSACTION - 각각의 트랜잭션이 어떤 SQL 쿼리를 실행하고 있는지 파악
  2. HOLDS THE LOCKS(S) - 해당 트랜잭션이 어떤 레코드/인덱스를 점유(락 보유) 하고 있는지 확인
  3. WAITING FOR THIS LOCK TO BE GRANTED - 어떤 레코드를 점유하려다 대기 상태에 빠졌는지 확인
  4. 트랜잭션 간 연결 분석 - 어떤 트랜잭션이 락을 잡고, 락을 기다리는지 관계를 분석
  5. WE ROLL BACK TRANSACTION (번호) - 어떤 트랜잭션이 롤백되었는지 확인

 

Deadlock 발생 원인

  • Deadlock이 발생한 트랜잭션은 아래와 같음
  • 비동기 트랜잭션이 특정 유저에 의해 동시에 같은 로직을 타서 발생
  • 동일한 레코드를 삭제하고, 새롭게 삽입하는 덮어쓰기 로직

 

START TRANSACTION;

DELETE FROM SAMPLE_TABLE
 WHERE 조건1
   AND EXISTS (조건2);

INSERT INTO SAMPLE_TABLE (...) VALUES (...), (...);

COMMIT;

 

트랜잭션 관계를 시간흐름에 맞춰 아래와 같이 정리할 수 있음:

 

시간 흐름 트랜잭션 1 (TX1) 트랜잭션 2 (TX2)
T0   DELETE 실행 (heap 158 삭제 플래그 설정)
T1   INSERT 준비 (heap 158에 삽입 시도)
T2   Gap 정리 대기 (heap 158 비우기 시도)
T3 DELETE 실행 시도 (heap 158 접근)  
T4 heap 158 락 획득 시도 (대기 중)  
T5   Gap 정리 완료 못하고 대기 계속
T6 락 대기 중 교착 발생 락 대기 중 교착 발생
T7 롤백 대상 선정 (InnoDB가 TX1 롤백)  

 

여기서 알아야할 중요한 포인트 두 가지:

  • 미세한 Gap 정리 타이밍 중 다른 트랜잭션이 들어오면 데드락이 발생
  • 다른 트랜잭션의 wait이 없었다면 하나의 트랜잭션 내에서 삭제-삽입 로직은 정상적으로 실행됨

 

 

빈도를 낮추는 방법

 

  • 여러 트랜잭션이 동시에 같은 데이터에 접근하면 충돌은 자연스럽게 발생
    • InnoDB는 이 충돌을 감지해서 무한정 대기하지 않게 한쪽을 롤백시키는 메커니즘을 갖음
    • 따라서, 데드락은 문제라기 보단 정상적인 복구 절차에 해당
    • 문제는 빈도와 복구를 제어하는 것

 

1. 쿼리 분리: DELETE → COMMIT → INSERT

  • 삭제와 삽입을 다른 트랜잭션 내에서 처리되도록 하는 방법
    • 이 경우 어플리케이션 복잡성이 증가
    • 복잡성1) 삽입에서 실패하는 경우, 재시도에 대한 로직이 필요
    • 복잡성2) 동기적인 방식이면 클라이언트에게 어떻게 응답할지에 대한 추가적인 고민이 필요
  • 이 방식은 비즈니스 트랜잭션 단위의 원자성을 잃을 수 있으므로 신중히 고려

 

2. 트랜잭션 락 획득 순서를 고정

방법 설명 특징
항상 같은 조건, 같은 순서로 데이터를 조작 DELETE → INSERT → UPDATE 처럼 항상 동일한 작업 순서를 지키기 - SQL 실행 순서 고정
- 메서드 내부 논리 순서 고정
락 잡는 순서를 고정 여러 테이블을 접근할 때 A → B 테이블 순서로 항상 락을 잡는다 - DB Deadlock 예방에 중요
- 예를 들어 항상 작은 ID → 큰 ID 순으로 처리
ORDER BY 강제 적용 DELETE, UPDATE 쿼리에도 필요하면 ORDER BY 사용해서 락 순서를 예측 가능하게 만든다 - 특히 다중 row 업데이트 시 유용
Composite Key 기준 고정 복합키 기반이라면 항상 키1 → 키2 → 키3 순서로 조건을 쓴다 - 인덱스 타고 락 걸리는 순서가 예측 가능

 

정렬에 대한 예시:

  • 여러 사람 데이터를 수정하는 트랜잭션이면
    • 항상 ID 오름차순으로 수정
  • 여러 테이블을 조작하는 트랜잭션이면
    • 항상 테이블 A → B → C 순서로 조작
  • DELETE/INSERT라면
    • 항상 DELETE를 먼저 하고, 삽입은 정해진 인덱스 순서로 INSERT

 

3. 트랜잭션 범위를 최소화

@Service
public class MyService {

    // 트랜잭션 없는 읽기 메서드
    public SomeDto readData(Long id) {
        return myRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException());
    }

    // 트랜잭션 필요한 쓰기 메서드
    @Transactional
    public void deleteAndInsert(Long id, NewData data) {
        myRepository.deleteById(id);
        myRepository.save(data);
    }

    // 서비스 진입 메서드
    public void process(Long id, NewData data) {
        SomeDto dto = readData(id); // 트랜잭션 없이 조회
        deleteAndInsert(id, data);  // 트랜잭션 안에서 삭제 -> 삽입
    }
}
  • 단순한 조회와 조립 로직에서는 트랜잭션이 필요없는 경우 분리하여 트랜잭션 범위를 최소화
  • 삭제 -> 삽입 메서드에 트랜잭션을 걸어 비즈니스 관점의 원자성을 지킴
  • 참고) Spring에서는 실제 DB 접근할 때 트랜잭션이 진짜 시작됨

 

 

모니터링 전략 - Deadlock 감지 및 로깅

 

1. 애플리케이션 단에서 처리

 

다음과 같은 플로우로 테이블에 저장:

  1. 트랜잭션 시작
  2. DELETE/INSERT 시도 → Deadlock 감지 → InnoDB가 자동 롤백
  3. 트랜잭션 소멸
  4. 예외 캐치 (DeadlockLoserDataAccessException, SQLException 40001)
  5. 예외 캐치 블록 안에서 새로 트랜잭션 시작해서 기록하거나 로그 남김

 

DB에서 Deadlock 발생시 Spring 애플리케이션 레벨에서 (JDBC, ORM, MyBatis, JPA 등) 아래와 같은 예외를 감지하여 처리:

 

예외 설명
DeadlockLoserDataAccessException Spring DataAccessException 중 하나, Deadlock 감지 시 발생
MySQLTransactionRollbackException MySQL JDBC 드라이버가 Deadlock 에러코드를 보고 던지는 예외
SQLState 40001 Deadlock 발생 시 SQL 표준 코드 - ERROR 1213 (40001): Deadlock found when trying to get lock

 

 

@Service
public class MyService {

    @Transactional
    public void processSomething() {
        try {
            doDelete();
            doInsert();
        } catch (DeadlockLoserDataAccessException ex) {
            deadlockLoggingService.saveDeadlockEvent(ex); // 새 트랜잭션으로 기록
            throw ex; // 원래 예외를 다시 던짐 (필요 시 재시도 로직 추가)
        }
    }
}

 

  • 발생시각, 트랜잭션 종류, Deadlock 에러 메시지 등을 특정 테이블 저장 혹은 로깅
  • 새 트랜잭션 (@Transactional(propagation = REQUIRES_NEW)) 으로 열어야 안전

 

요약

 

  • Deadlock은 시스템이 스스로 복구하는 정상 작동 중 하나
  • 트랜잭션은 짧게, 순서는 고정, 충돌은 피하되 재시도는 필수
  • 보통 DELETE → INSERT는 안전하지만, 갭락의 짧은 정리 타이밍을 항상 고려
  • 모든 긴 트랜잭션은 데드락과 lock wait timeout 리스크를 내포함

 

레퍼런스

  1. InnoDB deadlock on SELECT? Not possible! Or is it? | Shinguz (2023, FROMDUAL)
  2. How to detect deadlocks in Mysql/innodb? | msanford (2014, StackOverFlow)
  3. Performance Insight 써도 돼요? | 이재웅 (2018, 우아한기술블로그)
  4. 17.7 InnoDB Locking and Transaction Model | MySQL 8.4 Reference Manual