이전 글에서 SQL 표준 관점에서 Lock을 살펴보았고, 이번에는 MySQL(InnoDB)를 기준으로 Lock의 실제 동작 방식을 구체적인 예시와 함께 살펴보았다. 직접 쿼리를 실행하며 확인한 결과를 바탕으로 정리해 보았다.
✅ MySQL 엔진의 Lock
🔽 참고 MySQL은 구조적으로 MySQL 엔진과 스토리지 엔진의 두 계층으로 구성되어 있다. 아래 그림에서 볼 수 있듯이, MySQL 엔진 락은 비교적 레거시에 가까운 개념이며, 현대 MySQL에서는 대부분의 동시성 제어가 InnoDB 스토리지 엔진의 락을 중심으로 이루어진다.
▶ 글로벌 락(Global Lock)
데이터베이스 단위로 설정되는 락이다.
MySQL에서 제공하는 락 중 가장 범위가 크다.
MySQL 서버 전체에 걸쳐 모든 데이터베이스와 테이블의 DDL/DML 작업을 차단한다.
과거 MyISAM 엔진 기반에서는 mysqldump 백업 시 주로 사용했으나, InnoDB가 기본 스토리지 엔진으로 자리 잡으면서 XtraBackup, Enterprise Backup과 같은 대체 수단이 활용되고 있다.
🔽 쿼리 (명시적)
-- 글로벌 락 획득
FLUSH TABLES WITH READ LOCK;
-- 글로벌 락 해제
UNLOCK TABLES;
🔽 결과
세션 A에서 글로벌 락 획득 O세션 B에서 읽기 O세션 B에서 쓰기 X
▶ 테이블 락(Table Lock)
테이블 단위로 설정되는 락이다.
MyISAM과 같은 비트랜잭션 엔진에서 자주 사용되지만, InnoDB는 자체 트랜잭션 락을 제공하기 때문에 거의 사용되지 않는다.
다중 사용자 환경에서는 병목을 유발할 수 있다.
🔽 쿼리 (명시적)
-- 테이블 읽기 락 획득
LOCK TABLES users READ;
-- 테이블 쓰기 락 획득
LOCK TABLES users WRITE;
-- 테이블 락 해제
UNLOCK TABLES;
🔽 결과 (읽기 락)
세션 A에서 테이블 읽기 락 획득 O세션 B에서 읽기 O세션 B에서 쓰기 X
🔽 결과 (쓰기 락)
세션 A에서 테이블 쓰기 락 획득 O세션 B에서 읽기 X세션 B에서 쓰기 X
▶ 네임드 락(Named Lock)
대상이 특정 테이블이나 레코드가 아닌, 사용자가 지정한 문자열 단위로 설정되는 락이다.
데이터베이스 객체에 국한되지 않고 애플리케이션 레벨 동기화에 활용할 수 있다.
예를 들어, 특정 비즈니스 로직 단위로 하나의 자원에 접근을 제한할 때 사용된다.
네임드 락은 자주 사용되지 않는다.
🔽 쿼리 (명시적)
-- 'task_123' 라는 이름의 락을 10초 동안 시도
SELECT GET_LOCK('task_123', 10);
-- 락 해제
SELECT RELEASE_LOCK('task_123');
🔽 결과
세션 A에서 네임드 락 획득 O세션 B에서 네임드 락 획득 X
▶ 메타데이터 락(Metadata Lock)
테이블의 DDL 또는 DML 시 자동으로 획득되는 락이다.
목적은 DDL과 DML의 충돌을 방지하기 위한 것이다.
사용자가 직접 명령어로 제어하지 않으며, MySQL 엔진이 내부적으로 관리한다.
🔽 쿼리 (암묵적)
-- 세션 A (Metadata Lock 획득)
SELECT * FROM users;
-- 세션 B (세션 A의 COMMIT 전 시도 시 대기 상태)
ALTER TABLE users ADD COLUMN nickname VARCHAR(50);
🔽 결과
세션 A에서 DML 후 COMMIT X세션 B에서 DDL 반영 X
✅ InnoDB 엔진의 Lock
InnoDB는 트랜잭션과 MVCC 기반 동시성 제어를 위해 더 세밀한 레벨의 락을 제공한다.
▶ 레코드 락(Record Lock)
인덱스의 특정 레코드에 설정되는 락이다.
다른 DBMS의 레코드 락과 유사하지만, InnoDB는 테이블의 레코드가 아니라 인덱스의 레코드를 잠근다.
PK나 Unique Index가 있는 경우 정확히 한 레코드에만 적용된다.
인덱스가 전혀 없는 테이블이라도, InnoDB는 내부적으로 생성되는 클러스터 인덱스(Clustered Index)를 이용해 잠금을 관리한다.
🔽 쿼리 (암묵적)
-- 세션 A (PK 기준으로 한 행의 Record Lock 획득)
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 세션 B (세션 A의 COMMIT 전 시도 시 대기 상태)
UPDATE users SET age = 35 WHERE id = 10;
🔽 결과
세션 A에서 SELECT 후 COMMIT X세션 B에서 UPDATE 반영 X
▶ 갭 락(Gap Lock)
인덱스 레코드 사이의 간격(범위)에 설정되는 락이다.
특정 값이 존재하지 않는 인덱스 구간에 락을 걸어, 다른 트랜잭션이 해당 범위에 새로운 레코드를 INSERT하지 못하도록 막는다.
주된 목적은 Phantom Read를 방지하는 것이다.
🔽 쿼리 (암묵적)
-- 세션 A (age > 30인 구간에 Gap Lock 획득)
SELECT * FROM users WHERE age > 30 FOR UPDATE;
-- 세션 B (세션 A의 COMMIT 전 시도 시 대기 상태)
INSERT INTO users (name, age) VALUES ('Boogie', 35);
🔽 결과
세션 A에서 범위 SELECT 후 COMMIT X세션 B에서 INSERT 반영 X
▶ 넥스트 키 락(Next-Key Lock)
레코드 락(Record Lock)과 갭 락(Gap Lock)을 결합한 형태의 락이다.
이 락의 주 목적은 Phantom Read를 완전히 방지하고, 바이너리 로그(binlog) 기반 복제 환경에서 일관된 결과를 보장하는 것이다.
InnoDB에서는 기본적으로 REPEATABLE READ 격리 수준에서 SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE를 실행할 시 Next-Key Lock을 사용한다.
🔽 쿼리 (암묵적)
-- 세션 A (20~29 사이 구간에 Next-Key Lock 획득)
SELECT * FROM users WHERE age BETWEEN 20 AND 29 FOR UPDATE;
-- 세션 B (세션 A의 COMMIT 전 시도 시 대기 상태)
INSERT INTO users (name, age) VALUES ('Miso', 23);
🔽 결과
세션 A에서 범위 SELECT 후 COMMIT X세션 B에서 INSERT 반영 X
▶ 자동 증가 락(Auto Increment Lock)
AUTO_INCREMENT 컬럼에 값을 INSERT할 때 사용되는 테이블 단위 락이다.
AUTO_INCREMENT 값이 중복되거나 충돌하지 않도록 한 번에 하나의 트랜잭션만 시퀀스를 증가시킬 수 있도록 보장한다.
MySQL 8.0부터는 락 방식이 개선되어 병목 현상이 크게 완화되었다.
설정 변수 innodb_autoinc_lock_mode의 기본값이 2(Interleaved)로 되어 있으며, 이 모드에서는 INSERT 실행 시점에만 잠깐 테이블을 잠갔다가 즉시 해제한다.
따라서 이전 버전처럼 트랜잭션이 끝날 때까지 락이 유지되지 않아 병목 현상이 거의 발생하지 않는다.
🔽 쿼리 (암묵적)
-- 해당 값은 MySQL 설정 파일에서 변경
-- innodb_autoinc_lock_mode = 2;
-- 세션 A (AUTO_INCREMENT Lock 획득 (테이블 단위))
INSERT INTO users (name, age) VALUES ('Miso', 23);
-- 세션 B (세션 A의 COMMIT 전 시도 시 대기 상태)
INSERT INTO users (name, age) VALUES ('Boogie', 20);
🔽 결과
세션 A에서 INSERT 후 COMMIT X세션 B에서 INSERT 반영 O, innodb_autoinc_lock_mode = 2 (Interleaved 모드)이기 때문
▶ 공유 락(Shared Lock)
조회(SELECT) 작업 시 사용되는 락이다.
여러 트랜잭션이 동시에 같은 레코드에 대해 공유 락을 획득할 수 있다.
읽기는 여러 트랜잭션에서 동시에 가능하다.
그러나 다른 트랜잭션이 배타 락을 거는 것은 허용되지 않는다. 즉, 쓰기는 불가능하다.
🔽 쿼리 (명시적)
-- MySQL 8.0 이전
-- SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 세션 A (Shared Lock 획득)
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- 세션 B (세션 A의 Lock 해제 전에도 조회 가능)
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- 세션 B (세션 A의 Lock 해제 전 시도 시 대기 상태)
UPDATE users SET age = age + 1 WHERE id = 1;
🔽 결과
세션 A에서 공유 락 획득 O세션 B에서 읽기 O세션 B에서 쓰기 X
▶ 배타 락(Exclusive Lock)
변경(INSERT, UPDATE, DELETE) 작업 시 사용되는 락이다.
한 트랜잭션이 배타 락을 획득하면, 다른 트랜잭션은 해당 레코드에 접근할 수 없다.
즉, 읽기와 쓰기 모두 차단된다.
🔽 쿼리 (명시적)
-- 세션 A (Exclusive Lock 획득)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 세션 B (세션 A의 Lock 해제 전 시도 시 대기 상태)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 세션 B (세션 A의 Lock 해제 전 시도 시 대기 상태)
UPDATE users SET age = age + 1 WHERE id = 1;
🔽 결과
세션 A에서 공유 락 획득 O세션 B에서 읽기 X세션 B에서 쓰기 X
▶ 의도 락(Intention Lock)
테이블 단위 락과 레코드 단위 락 간 충돌을 방지하기 위한 락이다.
종류
IS (Intention Shared): 테이블 내 특정 행에 공유 락(S)을 걸 예정임을 표시한다.
IX (Intention Exclusive): 테이블 내 특정 행에 배타 락(X)을 걸 예정임을 표시한다.
SIX (Shared with Intent Exclusive): 테이블 전체에는 공유 락(S)을 걸고, 특정 행에는 배타 락(X)을 걸 예정임을 표시한다.
🔽 쿼리 (암묵적)
-- IS (Intention Shared)
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- IX (Intention Exclusive)
START TRANSACTION;
SELECT * FROM users WHERE id = 2 FOR UPDATE;
-- SIX (Shared with Intent Exclusive)
START TRANSACTION;
SELECT * FROM users FOR SHARE; -- 테이블 전체에 S 락 계열 걸림
UPDATE users SET age = age + 1 WHERE id = 1; -- 특정 행에 X 락 걸림
🔽 결과
IS 의도 락 획득 후 상태 직접 확인IX 의도 락 획득 후 상태 직접 확인SIX 의도 락 획득 후 상태 직접 확인
▶ 삽입 의도 락(Insert Intention Lock)
INSERT 시 해당 위치에 새로운 레코드를 삽입할 예정임을 표시하는 락이다.
단순히 이 자리에 INSERT 가능 여부만 확인하기 위해 존재한다.
서로 다른 위치에 삽입하려는 트랜잭션끼리는 대기하지 않도록 설계되었다.
🔽 쿼리 (암묵적)
-- 세션 A ((20, 40] 구간에 Next-Key Lock 획득)
START TRANSACTION;
SELECT * FROM users WHERE age BETWEEN 20 AND 40 FOR UPDATE;
-- 세션 B (세션 A의 Lock 해제 전 시도 시 대기 상태)
INSERT INTO users (name, age) VALUES ('Miso', 23);
🔽 결과
Insert 의도 락 획득 후 상태 직접 확인
▶ 조건 락(Predicate Lock for Spatial Indexes)
공간(Spatial) 인덱스 사용 시, 범위 조건에 따라 설정되는 락이다.
일반적인 레코드 락과 달리 조건 기반(Predicate)으로 락을 설정한다.
공간 좌표 기반 쿼리에서 동시성 제어 및 일관성 보장을 위해 사용된다.
예를 들어 ST_GeomFromText() 함수나 GEOMETRY, POINT, POLYGON 등의 공간 데이터 타입을 다룰 때 자동으로 설정된다.