home
Real MySQL 8.0

[5장 트랜잭션과 잠금] MySQL 엔진의 잠금

MySQL 엔진 레벨에서 제공하는 글로벌 락, 테이블 락, 네임드 락, 메타데이터 락이 각각 무엇을 잠그고 언제 쓰이는지 정리한다.

MySQL에서 사용되는 잠금은 크게 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나뉜다. 여기서 MySQL 엔진은 MySQL 서버에서 스토리지 엔진을 제외한 나머지 부분을 가리킨다. 두 레벨은 영향 범위가 다르다. MySQL 엔진 레벨의 잠금은 모든 스토리지 엔진에 영향을 미치지만, 스토리지 엔진 레벨의 잠금은 스토리지 엔진끼리 서로 영향을 주지 않는다.

이 글에서는 MySQL 엔진 레벨의 잠금을 다룬다. 서버 전체에 걸리는 글로벌 락, 테이블 데이터를 동기화하는 테이블 락, 사용자가 필요에 맞게 직접 쓰는 네임드 락, 테이블 구조를 잠그는 메타데이터 락이 여기에 속한다.

글로벌 락

글로벌 락은 MySQL이 제공하는 잠금 가운데 범위가 가장 크다. FLUSH TABLES WITH READ LOCK 명령으로 획득한다. 한 세션이 글로벌 락을 획득하면, 다른 세션에서 SELECT를 제외한 대부분의 DDL과 DML은 글로벌 락이 해제될 때까지 대기 상태로 남는다. 영향 범위가 MySQL 서버 전체라서, 작업 대상 테이블이나 데이터가 서로 다르더라도 똑같이 대기하게 된다.

이렇게 서버 전체를 멈추는 잠금은 여러 테이블을 일관된 시점으로 백업할 때 쓰였다. 하지만 그만큼 무겁다. 락을 잡는 동안에는 서버가 사실상 변경 작업을 받지 못하기 때문이다.

이 부담을 줄이기 위해 8.0에서 백업 락이 도입되었다. LOCK INSTANCE FOR BACKUP 명령으로 획득하고 UNLOCK INSTANCE로 해제한다. 백업 락은 일반적인 테이블의 데이터 변경(INSERT, UPDATE, DELETE)은 허용하고, 테이블 구조를 바꾸거나 계정 정보를 변경하는 등 백업을 깨뜨릴 수 있는 작업만 막는다. 덕분에 복제 소스의 변경을 계속 받아 적용하는 레플리카에서도, 백업을 뜨는 동안 복제를 멈추지 않아도 된다.

테이블 락

테이블 락은 개별 테이블 단위로 걸리는 잠금이다. 명시적으로 획득할 수도 있고, 쿼리 실행 과정에서 묵시적으로 걸리기도 한다.

명시적으로는 LOCK TABLES 명령으로 획득한다.

LOCK TABLES orders READ;   -- 읽기 잠금
LOCK TABLES orders WRITE;  -- 쓰기 잠금

획득한 잠금은 UNLOCK TABLES로 해제한다. 다만 명시적 테이블 락은 특별한 상황이 아니라면 애플리케이션에서 직접 쓸 일이 거의 없다. 테이블 전체를 잠그는 만큼 온라인 작업에 큰 영향을 주기 때문이다.

묵시적 테이블 락은 스토리지 엔진에 따라 다르게 동작한다. MyISAM이나 MEMORY 테이블에서는 데이터를 변경하는 쿼리를 실행하면 그 쿼리 단위로 테이블 락이 자동으로 걸린다. 반면 InnoDB는 스토리지 엔진 차원에서 레코드 기반의 잠금을 제공하기 때문에, 단순한 데이터 변경 쿼리로는 묵시적 테이블 락이 걸리지 않는다. 정확히는 InnoDB 테이블에도 테이블 락이 설정되기는 하지만, 대부분의 데이터 변경(DML) 쿼리에서는 무시되고 스키마를 변경하는 쿼리(DDL)에서만 영향을 준다.

네임드 락

네임드 락은 GET_LOCK() 함수로 임의의 문자열에 대해 거는 잠금이다. 앞의 잠금들과 다른 점은, 잠그는 대상이 테이블이나 레코드, AUTO_INCREMENT 같은 데이터베이스 객체가 아니라는 것이다. 단순히 사용자가 정한 문자열을 대상으로 획득하고 반납한다. 이 문자열은 DB 안에 실체가 있는 무언가가 아니라, 애플리케이션끼리 "이 이름을 잡은 쪽이 작업 권한을 가진다"고 약속한 표식일 뿐이다.

이 성질 때문에 네임드 락은 여러 서버나 세션에 걸친 작업을 하나만 실행되도록 조율하는 데 쓴다. 예를 들어 정산 배치 프로그램이 여러 대의 서버에 배포돼 있고, 매일 자정에 각 서버의 스케줄러가 동시에 배치를 실행한다고 하자. 아무 장치가 없으면 여러 서버가 같은 정산을 중복으로 돌린다. 이때 각 서버가 배치를 시작하기 전에 같은 이름의 네임드 락을 먼저 시도하게 만든다.

-- 획득에 성공하면 1, 이미 다른 세션이 잡고 있으면 (대기 없이) 0을 반환
SELECT GET_LOCK('batch_settlement_20260704', 0);

-- 1을 받은 서버만 정산 로직을 수행한 뒤

SELECT RELEASE_LOCK('batch_settlement_20260704');

GET_LOCK의 두 번째 인자는 대기할 초를 의미하고, 0은 기다리지 않고 즉시 결과를 받겠다는 뜻이다. 락을 얻은 한 서버만 1을 받아 정산을 수행하고, 나머지 서버는 0을 받아 그냥 건너뛴다. 잠그는 대상이 특정 테이블이 아니라 batch_settlement_20260704라는 문자열이므로, 어떤 테이블을 건드리는 작업이든 이 이름 하나로 전체의 실행 여부를 통제할 수 있다.

메타데이터 락

메타데이터 락은 테이블이나 뷰 같은 데이터베이스 객체의 이름이나 구조를 변경할 때 획득하는 잠금이다. 네임드 락과 달리 사용자가 명시적으로 획득하거나 해제하지 않는다. 이름이나 구조를 바꾸는 문장을 실행하면 자동으로 걸린다.

대표적인 상황은 테이블을 통째로 교체하는 작업이다. 실시간 순위를 담는 rank 테이블을 매일 새로 계산해 교체한다고 하자. 새 데이터를 미리 rank_new에 적재해 두고, 기존 테이블과 이름을 바꿔치기하려 한다. 이를 두 문장으로 나눠 실행하면 문제가 생긴다.

RENAME TABLE rank TO rank_old;      -- (A)
RENAME TABLE rank_new TO rank;      -- (B)

(A)와 (B) 사이의 짧은 순간에는 rank라는 이름의 테이블이 존재하지 않는다. 하필 그 틈에 들어온 쿼리는 "테이블이 없다"는 오류를 받는다.

RENAME TABLE은 여러 테이블의 이름 변경을 한 문장에 묶으면, 그 대상들의 메타데이터 락을 한꺼번에 원자적으로 획득한다.

RENAME TABLE rank TO rank_old, rank_new TO rank;

이렇게 하면 이름이 바뀌는 동안 rank가 사라지는 순간이 없어, 교체 도중에 들어온 쿼리도 오류 없이 처리된다.

운영에서는 메타데이터 락이 반대로 발목을 잡기도 한다. 어떤 테이블을 오래 읽고 있는 트랜잭션이 있으면 그 테이블의 메타데이터 락이 유지되는데, 이때 ALTER TABLE 같은 구조 변경 문장은 앞선 트랜잭션이 끝날 때까지 대기한다. 더 나쁜 것은, 대기 중인 ALTER 뒤에 도착한 평범한 조회 쿼리들까지 그 ALTER 뒤에 줄줄이 막힌다는 점이다. 그래서 스키마 변경은 긴 트랜잭션이 없는 시점을 골라 실행해야 한다.

댓글

아직 댓글이 없습니다.