본문 바로가기

Data/MySQL

[MySQL] 트랜잭션과 잠금 1편 — 락 메커니즘

트랜잭션이 안전하게 동작하려면 동시에 실행되는 다른 트랜잭션으로부터 데이터를 보호하는 장치가 필요합니다. MySQL은 이를 두 계층으로 구현합니다.

  • MySQL 엔진 락: MySQL 서버 계층에서 제공하는 락. 스토리지 엔진과 무관하게 동작합니다.
  • InnoDB 스토리지 엔진 락: InnoDB 내부에서 행(row) 단위로 동작하는 락입니다.

이번 포스트에서는 두 계층의 락 구조와, 락 경합이 발생했을 때 어떤 일이 생기는지를 살펴봅니다.

 


1. MySQL 엔진 락 (서버 레벨)

MySQL 엔진 락은 InnoDB가 기본 엔진이 된 이후로는 실무에서 직접 마주칠 일이 많지 않습니다. 그러나 MySQL 락 체계의 전체 그림을 이해하는 데 필요한 배경 지식입니다.

 

글로벌 락 (Global Lock)

MySQL 서버 전체에 걸리는 가장 범위가 넓은 락입니다. 글로벌 락이 걸리면 다른 세션은 DDL과 DML을 모두 실행할 수 없습니다. 주로 전체 데이터베이스를 백업할 때 사용합니다.

FLUSH TABLES WITH READ LOCK;

다만 InnoDB를 사용하는 경우, mysqldump--single-transaction 옵션을 지정하면 글로벌 락 없이도 일관된 스냅샷으로 백업할 수 있습니다. MVCC를 활용해 백업 시작 시점의 스냅샷을 읽기 때문입니다.

 

테이블 락 (Table Lock)

특정 테이블 전체에 락을 거는 방식입니다. 명시적으로 걸 수도 있고, 일부 DDL 작업에서 묵시적으로 걸리기도 합니다.

LOCK TABLES employees READ;   -- 읽기 락: 다른 세션의 쓰기 차단
LOCK TABLES employees WRITE;  -- 쓰기 락: 다른 세션의 읽기·쓰기 모두 차단

MyISAM은 행 레벨 락을 지원하지 않기 때문에 테이블 락이 동시성 제어의 전부였습니다. InnoDB에서는 트랜잭션과 행 레벨 락이 있기 때문에, 명시적으로 테이블 락을 걸어야 하는 경우는 거의 없습니다.

 

메타데이터 락 (Metadata Lock, MDL)

MDL은 테이블의 구조(스키마)를 보호하는 락입니다. 명시적으로 걸 수 없고, 테이블에 접근할 때 자동으로 걸립니다.

  • SELECT, INSERT, UPDATE, DELETE 실행 시 → MDL 읽기 락 획득
  • ALTER TABLE, DROP TABLE 실행 시 → MDL 쓰기 락 획득

 

MDL 읽기 락은 여러 세션이 동시에 보유할 수 있지만, MDL 쓰기 락은 배타적입니다. 따라서 장시간 실행 중인 트랜잭션이 있는 상태에서 ALTER TABLE을 실행하면, DDL이 MDL 쓰기 락을 대기하며 블로킹됩니다. 이 상태는 SHOW PROCESSLIST에서 Waiting for table metadata lock 메시지로 확인할 수 있습니다.

 

MDL은 트랜잭션이 종료될 때까지 유지됩니다. 커밋이나 롤백 없이 연결만 열어 둔 세션이 있으면 DDL 전체가 멈출 수 있으니 주의해야 합니다.


2. InnoDB 스토리지 엔진 락 (행 레벨)

InnoDB는 테이블 전체를 잠그는 대신, 실제로 접근하는 행에만 락을 걸어 동시성을 높입니다.

행 레벨 락을 살펴보기 전에 먼저 알아야 할 개념이 있습니다. InnoDB의 모든 행 레벨 락은 공유 락(S-Lock)배타 락(X-Lock) 중 하나로 동작합니다.

 

공유 락과 배타 락 (S-Lock / X-Lock)

공유 락(S-Lock)은 읽기 목적의 락입니다. FOR SHARE로 획득하며, 여러 트랜잭션이 동시에 보유할 수 있습니다.

배타 락(X-Lock)은 쓰기 목적의 락입니다. FOR UPDATE로 획득하며, 다른 트랜잭션의 S-Lock과 X-Lock 모두를 차단합니다.

SELECT * FROM employees WHERE id = 1 FOR SHARE;   -- S-Lock 획득
SELECT * FROM employees WHERE id = 1 FOR UPDATE;  -- X-Lock 획득
  S-Lock 요청 X-Lock 요청
S-Lock 보유 중 허용 차단
X-Lock 보유 중 차단 차단

 

S-Lock끼리는 공존할 수 있지만, X-Lock은 어떤 락과도 공존할 수 없습니다. UPDATE, DELETE, INSERT는 명시적 지정 없이도 항상 X-Lock을 획득합니다.

 

레코드 락 (Record Lock)

인덱스의 특정 레코드 하나에 거는 락입니다. InnoDB의 레코드 락은 실제 행이 아니라 인덱스 레코드에 걸립니다.

순수한 레코드 락은 = 조건 + PK 또는 유니크 인덱스 + 해당 레코드가 존재할 때만 걸립니다.

-- id가 PK일 때: id=1 레코드에만 X-Lock
SELECT * FROM employees WHERE id = 1 FOR UPDATE;

이 세 조건 중 하나라도 벗어나면 갭 락이 함께 걸리는 넥스트 키 락으로 동작합니다.

갭 락 (Gap Lock)

갭 락은 레코드 자체가 아니라 레코드와 레코드 사이의 빈 구간에 거는 락입니다. 다른 트랜잭션이 그 구간에 새로운 행을 INSERT하는 것을 막습니다.

 

id가 10, 20, 30인 레코드가 있을 때, id=20 레코드를 넥스트 키 락으로 잠그면 (10, 20] 구간이 잠깁니다. 이 상태에서 다른 트랜잭션이 id = 15를 INSERT하려 하면 갭 락에 의해 블로킹됩니다. 갭 락은 이렇게 팬텀 리드를 방지하는 역할을 합니다.

 

갭 락은 단독으로 걸리는 경우보다, 아래의 넥스트 키 락의 일부로 함께 걸리는 경우가 대부분입니다.

넥스트 키 락 (Next-Key Lock)

넥스트 키 락은 레코드 락 + 갭 락을 결합한 형태로, InnoDB가 범위 스캔 시 기본적으로 사용하는 락 방식입니다. 스캔한 레코드 자체와 그 레코드 앞의 갭을 함께 잠가, 다른 트랜잭션이 그 사이에 끼어드는 것을 원천 차단합니다.

id가 10, 20, 30인 레코드가 있을 때, id > 10 조건으로 조회하면 다음 구간에 락이 걸립니다.

SELECT * FROM employees WHERE id > 10 FOR UPDATE;
(10, 20]   ← id=20 레코드 + 10~20 사이 갭
(20, 30]   ← id=30 레코드 + 20~30 사이 갭
(30, +∞)   ← 30 이후의 갭 (갭 락만, 레코드 없음)

오른쪽이 닫힌 구간 ]인 이유는 해당 레코드 자체를 포함하기 때문입니다. 마지막 구간 (30, +∞)처럼 레코드가 없는 경우에는 갭 락만 적용됩니다.

 

어떤 락이 걸리는지는 연산자가 아니라 실제로 스캔하는 인덱스 범위가 결정합니다.

조건 인덱스 종류 걸리는 락
= PK / 유니크 인덱스, 레코드 존재 레코드 락만
= 일반 인덱스, 또는 레코드 없음 넥스트 키 락
>, <, BETWEEN 모든 인덱스 넥스트 키 락 (범위 전체)
인덱스 없는 컬럼 전체 레코드에 넥스트 키 락

 

인덱스 설계가 락 범위에 직접 영향을 주는 이유가 바로 여기에 있습니다.

MySQL의 기본 격리 수준인 REPEATABLE READ에서 팬텀 리드를 방지할 수 있는 이유도 이 넥스트 키 락 덕분입니다. 다음 포스트에서 격리 수준과 연결해서 자세히 설명합니다.

자동 증가 락 (Auto-Increment Lock)

AUTO_INCREMENT 컬럼을 가진 테이블에 INSERT할 때 자동으로 걸리는 특수한 락입니다. 여러 트랜잭션이 동시에 INSERT해도 AUTO_INCREMENT 값이 중복 없이 채번되도록 보장합니다.

 

일반 행 레벨 락과 달리 INSERT가 완료되는 즉시 해제됩니다. 트랜잭션이 롤백되더라도 이미 채번된 값은 반환되지 않으므로, AUTO_INCREMENT 값이 중간에 건너뛰는 현상은 정상입니다.

 


3. 락 대기와 데드락

락 대기 (Lock Wait)

한 트랜잭션이 다른 트랜잭션이 점유 중인 락을 기다리는 상태입니다. innodb_lock_wait_timeout (기본값: 50초) 이내에 락을 획득하지 못하면 대기 중인 트랜잭션은 자동으로 롤백됩니다.

 

현재 락 대기 상황은 다음 쿼리로 확인할 수 있습니다:

-- 어떤 트랜잭션이 어떤 락을 기다리고 있는지
SELECT * FROM performance_schema.data_lock_waits\G

-- 현재 보유 중인 락과 대기 중인 락 전체 목록
SELECT * FROM performance_schema.data_locks\G

 

데드락 (Deadlock)

두 트랜잭션이 서로가 점유한 락을 기다리는 순환 대기 상태입니다.

순서 트랜잭션 A 트랜잭션 B
1 id=1 락 획득 id=2 락 획득
2 id=2 락 요청 → 대기 id=1 락 요청 → 대기

A는 B가 놓아줄 id=2를 기다리고, B는 A가 놓아줄 id=1을 기다립니다. 어느 쪽도 먼저 양보하지 않으면 영원히 기다리게 됩니다.

 

InnoDB는 데드락을 자동으로 감지하고, 변경한 데이터 양이 더 적은 트랜잭션을 롤백해서 교착 상태를 해소합니다. 롤백된 트랜잭션은 애플리케이션에서 재시도하도록 설계하는 것이 일반적입니다.

 

 

데드락을 예방하려면 다음 원칙을 따릅니다:

  • 트랜잭션을 짧게 유지한다 — 락을 보유하는 시간을 최소화합니다.
  • 여러 행에 접근할 때 항상 같은 순서로 락을 획득한다 — 순환 대기 구조 자체를 만들지 않습니다.
  • 인덱스를 적절히 설계해 락 범위를 최소화한다 — 불필요하게 넓은 넥스트 키 락이 걸리지 않도록 합니다.

정리

락 종류 범위 락 타입 주요 목적
글로벌 락 서버 전체 전체 백업 시 쓰기 차단
테이블 락 테이블 전체 MyISAM 동시성 제어
메타데이터 락 테이블 스키마 DDL과 DML 동시 접근 방지
레코드 락 인덱스 레코드 1건 S / X 특정 행 수정 충돌 방지
갭 락 레코드 사이 구간 S / X 구간 내 INSERT 차단
넥스트 키 락 레코드 + 앞 갭 S / X 팬텀 리드 방지, REPEATABLE READ 구현
자동 증가 락 테이블 (즉시 해제) X AUTO_INCREMENT 채번 직렬화

 

다음 포스트에서는 트랜잭션 격리 수준 4가지와 MVCC를 살펴보고, 넥스트 키 락이 REPEATABLE READ를 어떻게 구현하는지 연결해서 설명합니다.