Next-Key Lock (MySQL)

Next-Key Lock(넥스트 키 락)은 MySQL의 InnoDB 스토리지 엔진에서 사용하는 잠금 메커니즘으로, 레코드 자체와 그 바로 다음 레코드 사이의 “갭”을 함께 잠그는 방식입니다. 이는 레코드 락과 갭 락을 결합한 형태로, 팬텀 리드(Phantom Read)를 방지하고 트랜잭션의 일관성을 유지하기 위해 사용됩니다. Next-Key Lock의

Overlay Image Overlay Image
« Back to Glossary Index

Next-Key Lock(넥스트 키 락)은 MySQL의 InnoDB 스토리지 엔진에서 사용하는 잠금 메커니즘으로, 레코드 자체와 그 바로 다음 레코드 사이의 “갭”을 함께 잠그는 방식입니다. 이는 레코드 락과 갭 락을 결합한 형태로, 팬텀 리드(Phantom Read)를 방지하고 트랜잭션의 일관성을 유지하기 위해 사용됩니다.

Next-Key Lock의 필요성

트랜잭션 격리 수준이 REPEATABLE READ 이상일 때, 팬텀 리드를 방지하기 위해 단순한 레코드 락만으로는 부족합니다. 다른 트랜잭션이 새로운 레코드를 삽입하여 기존의 쿼리 결과에 영향을 미치는 것을 막기 위해, InnoDB는 레코드와 그 다음 레코드 사이의 갭까지 잠그는 넥스트 키 락을 사용합니다.

Next-Key Lock의 동작 방식

넥스트 키 락은 인덱스 순서대로 레코드를 잠그며, 각 레코드와 그 다음 레코드 사이의 갭을 함께 잠급니다. 이는 다음과 같은 방식으로 작동합니다.

  • 레코드 락(Record Lock): 특정 인덱스 레코드 자체를 잠급니다.
  • 갭 락(Gap Lock): 인덱스 레코드 사이의 공간(갭)을 잠급니다.
  • 넥스트 키 락(Next-Key Lock): 레코드 락과 갭 락을 결합하여, 현재 레코드와 그 다음 레코드 사이를 잠급니다.

예제

테이블 생성 및 데이터 삽입

우선, 예제를 위해 간단한 테이블을 생성하고 데이터를 삽입합니다.

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    price DECIMAL(10, 2)
);

INSERT INTO products (id, name, price) VALUES
(10, '상품A', 1000.00),
(20, '상품B', 2000.00),
(30, '상품C', 3000.00),
(40, '상품D', 4000.00);

트랜잭션 A: 넥스트 키 락 획득

트랜잭션 A에서 다음과 같은 쿼리를 실행합니다.

-- 트랜잭션 A 시작
START TRANSACTION;

-- price가 1500 이상 3500 이하인 상품 조회 및 잠금
SELECT * FROM products WHERE price BETWEEN 1500 AND 3500 FOR UPDATE;

이 쿼리는 price가 1500 이상 3500 이하인 레코드를 조회하고, 해당 레코드들과 그 사이의 갭에 대해 넥스트 키 락을 획득합니다. 즉, 다음과 같은 범위가 잠금됩니다.

  • (10, 20]: price가 1000 초과 2000 이하인 범위
  • (20, 30]: price가 2000 초과 3000 이하인 범위
  • (30, 40]: price가 3000 초과 4000 이하인 범위

트랜잭션 B: 잠금된 범위에 대한 작업 시도

다른 세션에서 트랜잭션 B를 시작하고, 잠금된 범위 내에서 레코드 삽입이나 수정, 삭제를 시도합니다.

경우 1: 잠금된 범위에 새로운 레코드 삽입 시도

-- 트랜잭션 B 시작
START TRANSACTION;

-- price가 2500인 상품 삽입 시도
INSERT INTO products (id, name, price) VALUES (25, '상품E', 2500.00);

이 경우, 트랜잭션 B는 트랜잭션 A가 보유한 넥스트 키 락 때문에 블록됩니다. 트랜잭션 A가 커밋되거나 롤백될 때까지 대기합니다.

경우 2: 잠금된 레코드 수정 시도

-- 트랜잭션 B에서 price가 2000인 상품 수정 시도
UPDATE products SET price = 2100.00 WHERE id = 20;

이 경우에도 트랜잭션 B는 트랜잭션 A의 레코드 락에 의해 블록됩니다.

경우 3: 잠금되지 않은 범위에 레코드 삽입 시도

-- price가 4500인 상품 삽입 시도
INSERT INTO products (id, name, price) VALUES (50, '상품F', 4500.00);

이 경우, 잠금되지 않은 범위에 대한 작업이므로 트랜잭션 B는 정상적으로 실행됩니다.

트랜잭션 A 커밋 또는 롤백

트랜잭션 A를 커밋하거나 롤백하면 트랜잭션 B는 대기 상태에서 해제되어 다음과 같이 진행됩니다.

-- 트랜잭션 A 커밋
COMMIT;

이후 트랜잭션 B의 작업은 성공적으로 완료되거나, 제약 조건이나 다른 이유로 실패할 수 있습니다.

Next-Key Lock이 발생하는 상황

  • 범위 조건이 있는 SELECT … FOR UPDATE 또는 SELECT … LOCK IN SHARE MODE 쿼리
  • DELETE 또는 UPDATE 쿼리에서 범위 조건이 사용될 때
  • InnoDB의 격리 수준이 REPEATABLE READ 또는 SERIALIZABLE일 때

격리 수준과의 관계

넥스트 키 락은 REPEATABLE READ 이상 격리 수준에서 기본적으로 활성화됩니다.

  • READ COMMITTED: 레코드 락만 사용, 갭 락 및 넥스트 키 락 사용 안 함
  • REPEATABLE READ: 넥스트 키 락 사용, 팬텀 리드 방지
  • SERIALIZABLE: 넥스트 키 락 사용, 가장 엄격한 격리 수준

넥스트 키 락의 상세 동작 예시

인덱스와 넥스트 키 락

넥스트 키 락은 인덱스를 기반으로 작동합니다. 따라서 인덱스가 없는 컬럼에 대한 조건에서는 테이블 락이 발생할 수 있습니다.

예를 들어, price 컬럼에 인덱스가 없다고 가정하면, 위의 쿼리는 전체 테이블에 대해 잠금을 걸 수 있습니다.

-- price 컬럼에 인덱스 추가
CREATE INDEX idx_price ON products(price);

넥스트 키 락의 잠금 범위

인덱스 값 사이의 범위를 잠그므로, 범위 조건에 따라 잠금 범위가 달라집니다.

  • 등호 조건 (=): 해당 레코드와 그 다음 레코드 사이의 갭을 잠급니다.
  • 범위 조건 (<, >, BETWEEN 등): 조건에 맞는 모든 레코드와 그 사이의 갭을 잠급니다.

예제: 등호 조건에서의 넥스트 키 락

-- 트랜잭션 A에서 price가 2000인 상품을 잠금
SELECT * FROM products WHERE price = 2000.00 FOR UPDATE;

이 경우, price = 2000.00인 레코드와 그 다음 레코드 사이의 갭이 잠깁니다.

넥스트 키 락 회피 방법

  • 격리 수준을 READ COMMITTED로 변경: 하지만 팬텀 리드가 발생할 수 있습니다.
  • 잠금 범위를 최소화하도록 인덱스 설계: 필요 없는 컬럼에 대한 인덱스를 제거하거나, 필요한 컬럼에 적절한 인덱스를 추가합니다.
  • 명시적인 힌트 사용: FOR UPDATE 대신 LOCK IN SHARE MODE를 사용하여 잠금의 영향을 줄일 수 있습니다.

주의 사항

  • 교착 상태 발생 가능성: 넥스트 키 락은 교착 상태를 유발할 수 있으므로 트랜잭션 설계에 주의해야 합니다.
  • 인덱스의 중요성: 인덱스가 없을 경우 의도치 않은 범위에 잠금이 걸릴 수 있습니다.
  • 자동 증분(primary key): 자동 증가하는 기본 키에 대한 삽입은 일반적으로 다른 트랜잭션의 갭 락이나 넥스트 키 락에 의해 방해받지 않습니다.

결론

넥스트 키 락은 트랜잭션의 격리성과 데이터의 일관성을 유지하기 위한 중요한 메커니즘입니다. 그러나 불필요한 잠금으로 인한 성능 저하나 교착 상태를 피하기 위해서는 인덱스 설계, 트랜잭션 범위의 최소화 등 세심한 주의가 필요합니다.

추가 정보

  • 레코드 락과의 차이점: 레코드 락은 특정 레코드만 잠그지만, 넥스트 키 락은 그 레코드와 다음 레코드 사이의 갭까지 잠급니다.
  • 갭 락과의 관계: 넥스트 키 락은 갭 락의 특별한 형태로, 레코드 락과 갭 락을 결합한 것입니다.
  • 사용 시기: 일반적으로 넥스트 키 락은 자동으로 적용되며, 개발자가 직접 제어하지 않습니다. 하지만 트랜잭션 격리 수준과 쿼리의 형태에 따라 발생 여부가 결정됩니다.
« Back to Glossary Index