한 줄 정의

InnoDB는 MySQL 엔진과 별개로 레코드 기반의 잠금 을 제공하며, 레코드 자체가 아니라 인덱스를 잠그는 방식으로 동작합니다.

쉽게 말하면

도서관에서 책을 빌리는 상황을 생각해 보면:

  • 레코드 락 은 특정 책 한 권에 “대출 중” 표시를 거는 것입니다
  • 갭 락 은 책꽂이의 빈 자리에 “여기에 새 책 꽂지 마세요”라고 표시하는 것입니다
  • 넥스트 키 락 은 책 한 권 + 그 옆의 빈 자리까지 함께 잠그는 것입니다

그런데 InnoDB의 특이한 점은 책 자체가 아니라 “색인(인덱스)“을 잠근다 는 것입니다. 색인에서 검색되는 모든 항목에 잠금이 걸리므로, 인덱스 설계가 잠금 범위를 직접 결정합니다.

핵심 내용

InnoDB 잠금의 종류

flowchart LR
    subgraph 잠금_종류
        RL["레코드 락<br/>(Record Lock)<br/>특정 인덱스 레코드"]
        GL["갭 락<br/>(Gap Lock)<br/>레코드 사이 간격"]
        NKL["넥스트 키 락<br/>(Next Key Lock)<br/>= 레코드 락 + 갭 락"]
        AIL["자동 증가 락<br/>(Auto Increment Lock)<br/>AUTO_INCREMENT 채번"]
    end

레코드 락 (Record Lock)

레코드 자체만을 잠그는 잠금입니다. 다른 DBMS의 레코드 락과 역할은 같지만, InnoDB는 레코드가 아니라 인덱스의 레코드를 잠급니다.

  • 프라이머리 키 또는 유니크 인덱스에 의한 변경 작업에서는 갭 없이 레코드 자체에 대해서만 락을 겁니다
  • 인덱스가 하나도 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정합니다

갭 락 (Gap Lock)

레코드와 레코드 사이의 간격 을 잠그는 InnoDB 고유의 잠금입니다.

  • 간격에 새로운 레코드가 INSERT되는 것을 방지합니다
  • PHANTOM READ를 방지하기 위해 사용됩니다
  • 그 자체보다는 넥스트 키 락의 일부로 자주 사용됩니다

넥스트 키 락 (Next Key Lock)

레코드 락 + 갭 락 을 합쳐 놓은 형태입니다.

  • 바이너리 로그에 기록되는 쿼리가 레플리카 서버에서 동일한 결과를 만들어내도록 보장하는 것이 주목적입니다
  • 넥스트 키 락으로 인해 데드락이 발생하거나 다른 트랜잭션이 대기 하는 일이 자주 발생합니다
  • 바이너리 로그 포맷을 ROW 형태로 바꾸면 넥스트 키 락과 갭 락을 줄일 수 있습니다 (MySQL 8.0의 기본값이 ROW)

자동 증가 락 (Auto Increment Lock)

AUTO_INCREMENT 칼럼이 있는 테이블에서 동시 INSERT 시 중복 없이 순차적인 번호를 채번 하기 위한 테이블 수준 잠금입니다.

  • INSERT/REPLACE에서만 필요하며, UPDATE/DELETE에서는 걸리지 않습니다
  • 트랜잭션과 관계없이 값을 가져오는 순간만 잠기고 즉시 해제됩니다
innodb_autoinc_lock_mode 설정
모드동작MySQL 8.0 기본값
0전통 모드모든 INSERT에 자동 증가 락 사용
1연속 모드 (Consecutive)건수 예측 가능한 INSERT는 경량 래치 사용, INSERT … SELECT 등은 자동 증가 락 사용
2인터리빙 모드 (Interleaved)절대 자동 증가 락 사용 안 함, 경량 래치만 사용기본값
  • mode=2에서는 유니크한 값만 보장 하고, 연속성은 보장하지 않습니다
  • MySQL 8.0에서 기본값이 2로 변경된 이유: 바이너리 로그 기본 포맷이 ROW로 변경되어 연속성 보장이 불필요해졌기 때문입니다
  • STATEMENT 포맷의 바이너리 로그를 사용한다면 mode=1로 변경할 것을 권장합니다

인덱스와 잠금 — InnoDB에서 가장 중요한 개념

InnoDB의 잠금은 레코드를 잠그는 것이 아니라 인덱스를 잠그는 방식 으로 처리됩니다. 변경해야 할 레코드를 찾기 위해 검색한 인덱스의 레코드를 모두 잠급니다.

실전 예제
-- employees 테이블에 ix_firstname(first_name) 인덱스만 존재
-- first_name='Georgi'인 사원: 253명
-- first_name='Georgi' AND last_name='Klassen'인 사원: 1명
 
UPDATE employees SET hire_date=NOW()
WHERE first_name='Georgi' AND last_name='Klassen';

이 UPDATE는 1건만 변경 하지만, 인덱스를 이용할 수 있는 조건은 first_name='Georgi'뿐입니다. last_name은 인덱스에 없으므로, 253건 전체가 잠깁니다.

flowchart LR
    subgraph 인덱스_검색["ix_firstname 인덱스"]
        direction TB
        R1["Georgi, emp_no=1001 🔒"]
        R2["Georgi, emp_no=1002 🔒"]
        R3["... (253건 모두 잠금) 🔒"]
    end

    subgraph 실제_변경["실제 변경"]
        U1["Georgi Klassen ✅<br/>1건만 UPDATE"]
    end

    인덱스_검색 --> 실제_변경
인덱스가 없다면?

테이블에 인덱스가 하나도 없으면, 테이블 풀 스캔 이 발생하며 테이블의 모든 레코드(30만 건)가 잠깁니다. 이것이 MySQL에서 인덱스 설계가 성능뿐 아니라 동시성에 직결 되는 이유입니다.

레코드 수준의 잠금 확인 및 해제

MySQL 8.0에서는 performance_schemadata_locksdata_lock_waits 테이블로 잠금 상태를 확인할 수 있습니다.

-- 잠금 대기 순서 확인
SELECT
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM performance_schema.data_lock_waits w
    INNER JOIN information_schema.innodb_trx b
        ON b.trx_id = w.blocking_engine_transaction_id
    INNER JOIN information_schema.innodb_trx r
        ON r.trx_id = w.requesting_engine_transaction_id;

잠금을 가진 스레드가 오래 멈춰 있다면 강제 종료할 수 있습니다:

KILL 17;  -- 해당 스레드 번호

시각화

InnoDB 잠금 종류 비교

잠금 종류잠금 대상목적사용 시점
레코드 락인덱스 레코드특정 레코드 변경 보호PK/유니크 인덱스 변경 시
갭 락레코드 사이 간격새 레코드 INSERT 방지PHANTOM READ 방지
넥스트 키 락레코드 + 간격레코드 변경 + INSERT 방지보조 인덱스 기반 변경 시
자동 증가 락테이블 (AUTO_INCREMENT)순차적 번호 채번INSERT/REPLACE 시

내 생각

  • 인덱스 설계는 조회 성능만의 문제가 아닙니다. WHERE 조건에 적절한 인덱스가 없으면 UPDATE/DELETE가 불필요하게 많은 레코드를 잠그게 되고, 이는 다른 트랜잭션의 대기로 이어집니다. 인덱스를 설계할 때 “이 조건으로 UPDATE하는 쿼리가 있는가?”도 반드시 고려해야 합니다.

  • MySQL 8.0에서는 바이너리 로그 포맷이 ROW가 기본이므로, 넥스트 키 락으로 인한 불필요한 잠금이 많이 줄었습니다. STATEMENT 포맷을 사용 중이라면 ROW로의 전환을 검토할 가치가 있습니다.

관련 개념

출처

  • Real MySQL 8.0 (1권), 5.3 InnoDB 스토리지 엔진 잠금