트랜잭션 동시성 제어에 대해서 공부를 하는데 lock과 관련해서 많은 얘기가 나왔다. 생각보다 기본 개념이 안잡힌 부분들이 있는거 같아 이번 기회에 정리를 한 번 하고자 한다.
InnoDB는 두 가지 유형의 row-level locking을 수행한다.
트랜잭션 T1, T2가 있을 때,
T1이 특정 row를 read하기 위해 sLock를 가져갔다고 가정하자.
T2가 같은 row에 대해서 read하기 위해 sLock 가져갈 수 있다.
하지만 T2가 같은 row에 대해서 update/delete하기 위해 xLock를 가져갈 순 없다.
이번엔 T1이 특정 row를 update/delete하기 위해 xLock를 가져갔다고 가정하자.
T2가 같은 row에 대해서 sLock 혹은 xLock를 가져가려고 보낸 요청은 거절된다.
이 때, T2는 T1이 xLock을 놓도록 기다려야 한다.
Intention Lock은 table-level locking이다.
InnoDB는 row locking과 table locking의 공존을 허용하도록 다중 세분화 locking을 지원한다.
예로 LOCK TABLES … WRITE
명령어는 지정 테이블에 대해 xLock을 사용한다.
InnoDB는 locking을 다중 세분화 하기 위해 intention locks를 사용한다.
Intention Lock은 두 가지 유형이 있다.
예를 들어 SELECT … FOR SHARE
명령어는 IS를 설정하고,
SELECT … FOR UPDATE
명령어는 IX를 설정한다.
Intention Locking 프로토콜은 다음과 같다.
테이블 수준 락 호환성은 다음과 같다.
X | IX | S | IS | |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | Compatible | Conflict | Compatible |
S | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
Intention Lock의 주 목적은 누군가가 row에 대해 락을 잡고 있거나 테이블의 행을 락을 걸려고 하고 있다는 것을 보여주는 것이다. 이를 통해 테이블 데이터 정합성을 유지할 수 있다.
index record locking이다.
primary key가 있으면 이를 클러스터형 인덱스로 하여 해당 row에 lock을 건다.
primary key가 없고 UNIQUE NOT NULL 속성이 있으면 이를 클러스터형 인덱스로 한다.
Record Lock은 인덱스가 없어도 항상 인덱스 레코드 잠금을 한다.
이 경우는 InnoDB가 히든 클러스터형 인덱스(=기본키)를 만들고, 이 인덱스를 record locking을 위해 사용한다.
예를 들어
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
명령어를 실행하면 id=10인 레코드에 대해서 xLock을 건다.즉, row-level locking은 인덱스 레코드에 락을 거는 것이다.
Gap lock은 인덱스 레코드 사이의 간격에 락을 걸거나 첫번째 인덱스 레코드 앞이나 마지막 인덱스 레코드 뒤 간격에 락을 거는 것이다.(실제 인덱스 레코드에 lock을 거는 것이 아닌 인덱스 사이의 범위에 lock을 건다.)
예를 들어, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
명령어를 실행하면 다른 트랜잭션에서 c1=15인 값을 insert 할 수 없다.
Gap Lock은 순전히 갭에 삽입되는 것을 방지하는 것이 유일한 목적이다.
Gap Lock은 성능과 동시성 사이의 tradeoff이다.
일부 isolation level에선 사용되고, 어느 레벨에선 사용되지 않는다.(READ_COMMITTED에는 Repetable Read를 보장하지 않아도 되기에 사용되지 않는다.)
Next-Key Lock은 (인덱스 레코드에 대한 레코드 잠금 + 인덱스 레코드 앞의 간격에 대한 잠금)이다.
InnoDB 테이블 인덱스를 검색하거나 스캔할 때 발견한 인덱스 레코드에 sLock 혹은 xLock을 건다.
따라서 row-level locks은 실제로 index-record locks이다.
예를 들어, 인덱스에 값 10, 11, 13, 20이 포함되어 있다고 가정하자.
이 인덱스에 대해 가능한 다음 키 잠금은 다음 간격을 포함한다.
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
만약 select * from table where id >12 for update
를 실행하면
InnoDB는 Repetable_READ일 때(Default), next-key를 사용해 검색 및 인덱스 스캔을 하는데 발생할 수 있는 팬텀 리드를 방지한다.
Insert 행 삽입 전에 설정되는 일종의 Gap Lock이다.
일반적으로 Gap Lock은 Locking Read를 위한 Select 구문이 발생할 때 실행되는 반면, 이 Lock은 Insert시점에 자동으로 발생한다.
동일한 index Gap에 삽입되는 여러 트랜잭션이 Gap 내의 같은 위치에 삽입되지 않으면 기다릴 필요 없이 삽입되도록 하는 것이 주 목적이다. (Insert Intetion Lock끼리는 충돌하지 않는다.)
해당 row에 대해 xLock을 걸기 전에 먼저 Insert Intention Lock을 건다.
예를 들어, 클라이언트 A, B가 있다고 가정하자.
클라이언트 A는 두 개의 인덱스 레코드(90, 102)를 포함하는 테이블을 생성한다.
그 다음 ID가 100보다 큰 인덱스 레코드에 xLock을 설정하는 트랜잭션을 시작한다.
xLock엔 레코드 102앞에 갭 (90,102] 잠금이 포함된다.
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id |
+-----+
| 102 |
+-----+
클라이언트 B는 윗 간격 (90,102](90초과 102이하)에 레코드 101를 삽입하려고 한다.
트랜잭션은 xLock을 얻기위해 대기하는 동안 삽입 의도 잠금을 사용하여 락을 얻고 레코드 101을 삽입한다.
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);
AUTO_INCREMENT 컬럼에 값을 일관성있게 유지해주는 Lock이다.
한 트랜잭션에서 테이블에 데이터를 넣으면 다른 트랜잭션은 이를 기다리고, 앞선 트랜잭션이 연속적인 기본키 값을 받아 일관성을 유지시킨다.
InnoDB는 SPARTIAL 공간 데이터가 포함된 열의 인덱싱을 지원한다.
SPARTIAL 인덱스를 포함하는 작업에 대해 잠금을 처리하기 위해서 next-key 잠금은 REPEATABLE_READ 또는 SERIALIZABLE 레벨에서 원활히 작동하지 않을 수 있다.
다차원 데이터에는 명확한 순서가 없어 ‘Next’ 키가 무엇인지 명확하지 않기 때문이다.