본문 바로가기

Database

MySQL Gap Lock, Next key Lock (갭락, 넥스트 키락) 과 데드락

Real MySQL 8.0 을 보다가, Lock 과 Transaction isolation 에 대해 설명이 너무 부족한 것 같아 개인적으로 추가 정리를 해보고자 합니다. 그 중에서도 이번 포스팅은 MySQL 만의 특별한 락인 Gap Lock 을 다루어 보고자 합니다.

 

Gap Lock(갭락)

MySQL 은 레코드 기반의 잠금을 지원하여 주로 레코드 잠금(실제로는 레코드의 인덱스를 잠금)으로 동시성을 제어하는데,

갭 락은 실제 존재하는 레코드가 아닌, 레코드가 존재하지 않는 빈 공간에 대해서 거는 MySQL 만의 특별한 잠금이다.

 

ID(PK)
3
4
10

예시로 위와 같은 데이터가 있다고 치면,  (1~2) (5~9) (10~) 을 잠그는걸 갭 락이라고 한다.

또, 존재하는 레코드 인덱스에 대한 락 (3, 4, 10) 이 합쳐져서 사용되면 넥스트 키 락이라고 부른다.

일반적으로 갭락은 넥스트 키락으로서 활용된다.

그렇다면 MySQL 은 왜 이런 갭락을 사용하여 인덱스의 빈 공간을 잠그는 것일까?

 

  • Repeatable Read  의 보장
  • Replication 일관성 보장

 

Repeatable Read 보장

Repeatable Read 의 보장을 위한 Gap Lock 은, 범위 조회에서 일어난다.

위 테이블에 아래와 같은 두 트랜잭션 요청이 들어온다고 가정하자.

# Transaction 1
SELECT * FROM tb WHERE id < 7 LOCK IN SHARE MODE; --- (1)
SELECT * FROM tb WHERE id < 7 LOCK IN SHARE MODE; --- (3)
# Transaction 2
INSERT INTO tb(id) VALUES (1); --- (2)
INSERT INTO tb(id) VALUES (2);

1 -> 2 -> 3 순서로 쿼리가 실행되었을때,  1번과 3번은 동일한 쿼리지만 조회결과가 다를 것이다.

하지만 Repeatable Read 의 격리 수준에서는 7 이하의 Gap 에 대해 잠금을 걸기 때문에 2번 쿼리는 잠금이 해제될때 까지 대기하게된다.

 

Replication 일관성 보장

MySQL 의 Replication 은 바이너리 로그(binlog) 파일을 활용한다. Master 노드에서 바이너리 로그를 기록하면, Replication Master Thread 가 이를 읽어서 Replica 노드 쪽으로 전송하는 방식이다.  

 

binlog 의 기록 방식은 3가지가 존재한다. 

  • Statement: 실행한 SQL 구문 그대로를 저장하는 방식
  • Row: 변경작업으로 변경된 Row 데이터를 저장하는 방식
  • Mixed: Statement 와 Row 를 혼합한 방식이나 대부분의 경우 Row 방식으로 기록된다.

STATEMENT의 경우 SQL 구문을 그대로 binlog 에 쌓기 때문에 실행시점과 커밋시점이 매우 중요해지는데, 

이로 인한 Master 와 Replica 간 결과 불일치 현상을 제거하기 위해

Statement 는 5.0 버전 이후 부터는 REPEATABLE READ 조건이 필수조건으로 갭락을 사용하게 된다.

 

갭락 조건

갭락은 오직 Insert 를 막기위한 목적으로서 S Lock 의 형태로만 존재한다.

(performance_schema.data_locks 에 X, GAP 으로 표기되기도 하나, S Lock 의 개입을 막지 않는다.)

S Lock 의 형태로만 존재하기 때문에 큰 영향이 없을 것 같지만 운영상에서 가장 빈번하게 데드락을 발생시킨다고 한다.

갭락으로 인한 데드락을 살펴보기 전에 Index 종류별 발생하는 락을 살펴보자.

 

  • PK, Unique Index
    - 동등 조건으로 인한 결과가 단건임이 보장될 때, 해당 레코드에만 레코드락이 발생한다.
    - 단건임이 보장되지 않는경우 즉 동등 조건으로 없는 레코드를 조회하거나, 복합 인덱스 중 일부 컬럼만 사용한 경우를 포함한 결과에 대한 유일성을 보장하지 못하는 경우 넥스트 키락이 발생한다.
  • Non-Unique Secondary Index
    - 항상 넥스트 키락을 발생시킨다. 

 

갭락 으로 인한 데드락 상황

client-1 client-2
SELECT * FROM tb WHERE tb.id = 3 FOR UPDATE; SELECT * FROM tb WHERE tb.id = 3 FOR UPDATE;
DELETE FROM tb WHERE tb.id = 3; DELETE FROM tb WHERE tb.id = 3;
INSERT INTO tb(id, value) VALUES (3, 100);  
  INSERT INTO tb(id, value) VALUES (3, 100);

id 가 3번인 행이 존재한지 확인한 후, 있으면 삭제하고 새로 id 가 3인 Row 를 입력하는 로직을 구현한다고 가정하자.

이때, 3번이 실제로 존재하지 않는 id 이면 Record 락 없이 Gap 락을 걸게된다.

위에서 말했든 Gap 락은 S 락 형태만이 존재하므로, client 1,2 트랜잭션의 SELECT, DELETE 구문은 모두 잠금대기 없이 수행이 되고,

서로 각자 Gap 락을 걸게 된다.

 

 

위 사진은 세션 1번의 트랜잭션에서 DELETE 구문을 수행하고 performance_schema.data_locks 테이블을 조회한 결과이다.

위 사진은 세션 1번의 트랜잭션을 커밋하지 않은 상태에서 세션 2번의 트랜잭션을 DELETE 까지 수행했을때 lock 의 상태이다.

보시다시피 경합없이 모두 Gap 락이 GRANTED 되었다.

 

이때 각각의 트랜잭션에서 Gap Lock 이 걸린 인덱스에 대해 INSERT 하게되면, 서로의 잠금 해제를 대기하는 데드락상태에 빠지게 되는것이다. 살펴본 예제처럼 Gap Lock 에 의한 데드락은 사전에 예측하기도 어렵고 복잡한 경우가 많다.

 

Gap Lock 으로 인한 데드락을 회피하기 위해서 Gap Lock 자체의 발생빈도를 줄이도록 isolation level 을 READ-COMMITED 로 낮추고, binlog_format 을 ROW 로 변경하는 방법이 있지만 격리레벨을 변경하는것은 큰 이슈사항이므로 숙고해서 적용해야한다.