MySQL의 InnoDB는 데이터에 대한 동시 요청이 발생한 경우, 일종의 잠금(Lock)을 통해 무결성을 보장한다. 잠금을 구현하는 방식에 따라 공유락과 배타락으로 나눌 수 있으며, 잠금을 어느 범위로 거는지에 따라 레코드락과 갭락, 넥스트 키락으로 나눌 수 있다.
공유락은 트랜잭션이 특정 행에 대한
읽기
권한만을 가지도록 허용하는 잠금이다.
SELECT * FROM t WHERE id = 1 FOR SHARE;
FOR SHARE
키워드를 통해 id가 1번인 행에 대한 공유락을 획득 할 수 있다.단순 SELECT 문 만으로도 데이터를 조회 할 수 있는데, 굳이 공유락을 획득하는 이유는 무엇일까?
배타락은 트랜잭션이 특정 행에 대한
읽기 및 쓰기
권한을 가지도록 허용하는 잠금이다.
SELECT * FROM t WHERE id = 1 FOR UPDATE;
FOR UPDATE
키워드를 통해 id가 1번인 행에 대한 배타락을 획득 할 수 있다.트랜잭션 1이 공유락을 획득한 후, 트랜잭션 2가 추가적으로 공유락을 획득하려는 경우 (가능)
트랜잭션 1이 공유락을 획득한 후, 트랜잭션 2가 추가적으로 배타락을 획득하려는 경우 (불가능)
트랜잭션 1이 배타락을 획득한 후, 트랜잭션 2가 추가적으로 공유락을 획득하려는 경우 (불가능)
공유락과 배타락을 사용할 때, 데드락 발생 가능성에 대해 주의해야 한다.
데드락(Dead Lock)
이란, 서로가 점유하는 자원에 대한 무한정 대기 상태를 의미한다.
예를 들면 아래와 같은 상황에서 데드락이 발생 할 수 있다.
아래는 해당 과정을 직접 실행한 결과이다.
일반적으로 레코드락은 테이블의 레코드 자체에 대한 잠금을 의미한다.
MySQL의 InnoDB는 테이블의 레코드가 아닌 인덱스의 레코드를 잠근다는 것이 특징이다.
예를 들어, 아래와 같은 user 테이블이 있다고 해보자.
그리고 name이 “kyle”인 레코드를 대상으로 공유락을 획득한다.
SELECT * FROM user WHERE name = 'kyle' FOR SHARE;
이제 아래의 명령어를 통해 어떤 레코드에 어떤 잠금이 걸렸는지 확인해보자.
SELECT * FROM performance_schema.data_locks;
놀랍게도 name이 “kyle”인 레코드 뿐만 아니라, 전체 레코드에 공유락(S)이 걸린 것을 확인할 수 있다.
그렇다면 이제 name 컬럼에 인덱스를 생성하고 같은 작업을 반복해보면 어떻게 될까?
CREATE INDEX name_index ON user (name);
아까와는 다르게 name이 “kyle”인 세 개의 레코드에만 공유락(S)이 걸리는 것을 확인할 수 있다.
여기서 또 재밌는 점이 있는데, SELECT 문을 아래와 같이 살짝 바꿔보면 어떻게 될까?
SELECT * FROM user WHERE name = 'kyle' and age = 30 FOR SHARE;
하지만 실제로 결과를 확인해보면 이전과 같이 세 개의 레코드에 모두 공유락(S)이 걸리는 것을 확인할 수 있다.
인덱스를 기준
으로 잠금을 걸기 때문에, 인덱스에 포함되지 않은 컬럼을 조건으로 추가하여도 여전히 인덱스를 기준으로 레코드락을 걸게 된다.갭락은 단일 레코드가 아닌
레코드와 레코드 사이 혹은 인덱스의 맨 앞 혹은 끝의 바깥 범위
를 잠그는 것을 의미한다.
레코드락처럼 특정 데이터에 대한 잠금 용도로 사용하는 것이 아니라, 데이터의 생성, 수정, 삭제를 방지하는데 사용되는 잠금 방식이다.
InnoDB에서는 갭락만 따로 사용하지 않고, 레코드락과 함께 넥스트 키락이라는 개념으로 사용한다.
따라서 갭락이 어떻게 적용되는지 아래에서 자세하게 살펴보자.
넥스트 키락은
레코드락과 갭락을 함께 사용
하는 것을 의미한다.
실제로 InnoDB에서 불필요한 데이터의 생성, 수정, 삭제를 방지하는데 사용되며, REAPEATABLE READ 격리수준에서 적용된다.
예를 들어 다음과 같은 user 테이블이 있다고 하자.
name 컬럼에 대한 인덱스를 적용한다.
CREATE INDEX name_index ON user (name);
트랜잭션 1
에서 h로 시작하는 name을 가진 user들에 대해 배타락을 획득한다.
SELECT * FROM user WHERE name LIKE 'h%' FOR UPDATE;
트랜잭션 2
에서 “howard”라는 이름의 user를 생성(INSERT)한다.
INSERT INTO user (name, age) VALUE ('howard', 40);
이는 갭락 때문이다.
트랜잭션 1에서 인덱스의 범위 검색을 통해 조회한 레코드들에 배타락을 걸면, 해당 인덱스의 앞뒤로 데이터를 생성하지 못하도록 잠금을 거는데, 이를 갭락(Gap Lock)
이라고 한다.
종합하자면, 트랜잭션 1과 트랜잭션 2의 결과에 따른 잠금이 아래와 같이 나타나게 된다.
여기서 데이터의 순서는 데이터베이스 테이블의 레코드 순서가 아니라, B-Tree의 인덱스 순서임에 주의한다.
트랜잭션 1로 인해 haley, harry라는 인덱스에 레코드락이 걸리고, 해당 인덱스 전, 후, 사이로 데이터가 생성, 수정, 삭제되지 못하도록 갭락이 걸린다. 이런 방식을 넥스트 키락
이라고 한다. 따라서, howard가 생성 될 경우, 인덱스 정렬에 의해 harry의 다음에 위치하게 될 것이다. 하지만 갭락이 존재하므로 생성할 수 없어, 잠금이 풀릴 때까지 기다려야 한다. 이전의 경우 기다리는 시간이 너무 길기 때문에 timeout이 발생한 것이다.
그렇다면 만약 h로 시작하는 이름이 아니면, 갭락에 걸리지 않고 정상적으로 생성될까?
INSERT INTO user (name, age) VALUE ('steve', 10);
이는 h로 시작하는 name의 인덱스와 그 사이에만 넥스트 키락이 걸린 것이기 때문에, steve라는 인덱스는 넥스트 키락이 걸린 위치와 연관되어 있지 않아서 정상적으로 생성이 된다고 볼 수 있다.
여기서 더 재밌는 사실을 알 수 있는데, 그럼 h로 시작하지는 않지만 갭락이 있는 위치에 해당할만한 인덱스라면 어떨까?
INSERT INTO user (name, age) VALUE ('isaac', 15);
isaac은 인덱스에서 정렬될 때, harry 다음으로 들어가게 되므로 갭락에 의해 생성이 불가능하다.
지금까지 MySQL의 InnoDB는 레코드락과 갭락을 모두 사용하는 넥스트 키락 방식으로 잠금을 적용한다고 보았다. 그렇다면 왜 갭락이라는 것을 사용할까? 단순히 레코드락만으로도 동시 요청에 대한 데이터 무결성을 지킬 수 있지 않을까?
갭락의 필요성을 알기 위해, 일단 갭락을 사용하지 않는 환경에서 하나의 실험을 해보자.
MySQL에서 갭락을 사용하지 않으려면 트랜잭션 격리수준을 REPEATABLE READ
에서 READ COMMITTED
로 내리면 된다.
다시 아까와 같은 user 테이블이 있다고 해보자.
아래의 시나리오 대로 쿼리문을 실행해보자.
결과 1
결과 2
3번과 6번 항목 사이에는 트랜잭션 1에서 어떠한 쿼리문도 실행하지 않는다.
따라서 3번의 결과 1
과 6번의 결과 2
는 트랜잭션 1에서 동일한 모습으로 나타나야 한다.
일단 트랜잭션 1에서 3번을 실행하면 아래와 같은 결과 1
이 나타난다.
이후 트랜잭션 2에서 howard라는 이름의 user를 생성한 후 커밋한다.
-- 트랜잭션 2
INSERT INTO user (name, age) VALUE ('howard', 20);
COMMIT;
READ COMMITTED
이므로 갭락이 존재하지 않아, 정상적으로 생성된다.마지막으로 트랜잭션 1에서 6번을 실행하면 아래와 같은 결과 2
가 나타난다.
결과 1
에서는 haley, harry만 나왔는데, 결과 2
에서는 트랜잭션 2에서 추가한 howard까지 잠금 조회가 된다.결과 1
과 결과 2
사이에 어떠한 작업도 하지 않았는데, 동일한 쿼리문에 다른 결과가 나타나는 것이다.이렇게 하나의 트랜잭션 내에서 특정 레코드가 보였다 안보였다 하는 현상을 유령 읽기(Phantom Read)
라고 한다. 갭락이 존재하지 않는 READ COMMITTED
격리수준에서는 다른 트랜잭션에서 데이터를 생성하는 것을 막을 수 없다. 따라서 h로 시작하는 이름의 새 user를 생성할 수 있다. 잠금 조회를 할 때는 MySQL의 언두 로그(Undo Log)가 아닌 데이터베이스에서 직접 조회를 하므로(언두 로그는 잠금이 없기 때문) 새롭게 생성된 데이터까지 조회되는 것이다.
MySQL의 InnoDB 엔진은 기본 격리수준으로 REPEATABLE READ
를 채택하고 있으며, 여기에는 갭락을 포함한 넥스트 키락을 이용해 위와 같은 유령 읽기(Phantom Read) 현상을 방지하고 있다.