찾아보고, 이해가 안가는 부분은 직접 실행해보고, 내용을 제가 이해한대로 재해석했기 때문에 틀려진 부분이 있을 수 있습니다. 틀린 부분은 바로 잡아주시면 수정하겠습니다.
글을 시작하기 앞서 락의 수준과 종류를 간단하게 알면 전체 내용을 이해하기 훨씬 쉬울 것이다.
MySQL 서버 수준 : 서버 전체의 작업을 멈추고 진행하는 수준.
ex) FLUSH TABLES WITH READ LOCK
테이블 수준 : 특정 테이블의 작업들을 멈추고 진행하는 수준.
ex) ALTER TABLE tb_index RENAME column age TO levels;
레코드 수준 : 특정 레코드(들)의 작업을 멈추고 진행하는 수준.
ex) SELECT * FROM tb_test FOR UPDATE
락의 수준은 그냥 내가 이해하기 쉬우라고 나눠봤다.
이 글에서는 테이블 수준 하위에서 레코드들에 적용이 되는 락들 위주로 알아볼 것이다.
S-Lock(읽기락) : 다른 트랜잭션에서 조회 가능. 수정 불가능
ex) SELECT * FROM tb_test LOCK IN SHARE MODE
X-Lock(쓰기락) : 다른 트랜잭션에서 조회 불가능. 수정 불가능.
ex) SELECT * FROM tb_test FOR UPDATE
, INSERT
, UPDATE
, DELETE
레코드 수준의 S-Lock 쿼리를 실행시키면 쿼리를 실행한 테이블에 IS-Lock이 걸리고,
레코드 수준의 X-Lock 쿼리를 실행시키면 쿼리를 실행한 테이블에 IX-Lock이 걸린다.
IS, IX락은 모두 실질적인 작업을 하는 락이아니라 레코드에서 잠금을 가지고 처리할 것이라는 의도를 담고 있는 락이다.
이외에도 테이블 수준에서 발생할 수 있는 락들이 있지만 지금 글에서는 중요하지 않으니 생략한다.
SELECT ENGINE_TRANSACTION_ID, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS , LOCK_DATA FROM performance_schema.data_locks;
쿼리를 실행시키면 현재 락 정보를 확인할 수 있다.
이 결과에서 LOCK_TYPE이 RECORD라고 레코드 락을 의미하는게 아니라 레코드 수준의 락이라는 의미이니 오해하지 말자.
레코드 락은 레코드 자체만을 잠그는 것을 의미한다.
우리가 앞서 언급한 S-Lock, X-Lock을 생각하면 쉽다.
하지만 여기서 중요한 부분은 InnoDB 스토리지 엔진은 레코드 자체가 아니라 인덱스의 레코드를 잠근다는 것이다.
예시 테이블을 통해서 인덱스의 레코드를 잠그는 것을 확인해보자.
tb_index 테이블에는 다음과 같은 데이터들이 있고, age
컬럼에 세컨더리(B트리) 인덱스가 걸려있다.
select * from tb_index where age = 1 FOR UPDATE;
이 쿼리를 실행하면 어떤 레코드에 락이 걸릴까?
IX락이 걸렸고, age 인덱스에서 age값이 1이고 id가 1,4,6인 레코드에 잠금이 걸렸다.
그리고 PK인덱스의 1, 4, 6 레코드에 잠금이 걸렸다.
age가 1에 관련된 레코드가 모두 잠긴 것이다.
락 정보 마지막에 X,GAP
이라는 잠금도 있는데 이것은 갭락으로 뒤에서 확인할 것이다.
이전 쿼리에 name='현우'
조건을 추가해봤다.
select * from tb_index where age = 1 and name='현우' FOR UPDATE
여기선 조건에 부합하는 id가 1인 레코드만 잠기지 않을까?
그렇지 않다. 여전히 age가 1이고 id가 1, 4, 6인 레코드들이 잠겼다.
"레코드 자체가 아니라 인덱스의 레코드를 잠근다"
세컨더리 인덱스로 등록된 age
를 기준으로 age
가 1인 레코드들이 전부 잠긴 것이다.
만약 age
가 1인 다른 레코드가 2000개가 있다면 2000개의 레코드가 모두 잠길 것이다.
이런 상황의 테이블이 실제 서비스에 적용되어 여러 스레드가 UPDATE
를 한다면 어떨까?
UPDATE
를 위한 적절한 인덱스가 준비돼 있지 않다면, 한 스레드에서 여러 레코드들의 락을 점유하게 되어 동시성이 상당히 떨어질 수 있다.
이것이 MySQL의 방식이며, MySQL의 InnoDB에서 인덱스 설계가 중요한 이유 또한 이것이다.
PK 또는 유니크 인덱스 작업에만 단일 레코드에 락을 건다.
select * from tb_index where id = 1 FOR UPDATE;
이렇게 PK나 유니크 필드를 지정했을 때는 깔끔하게 레코드에 락이 걸린다.
갭 락은 레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미한다.
이전에 select * from tb_index where age = 1 FOR UPDATE;
를 했을 때 락 정보이다.
이번에 집중할 부분은 X,GAP
부분이다.
다음 사진은 데이터를 age를 기준으로 정렬해놓은 것이다.
age 인덱스는 age를 기준으로 사진과 같이 데이터를 정렬해 놓았을 것이다.
name 정보는 없겠지만 이해를 위해 함께 놓았다.
락 정보에 X,GAP
정보를 다시 확인해보자. LOCK_DATA를 보면 3, 2로 나와있다.
이는 age 인덱스에 age 값이 3으로 돼있는 id가 2인 레코드까지 갭락이 걸렸다는 것이다.
즉 1~3 사이의 간격이 갭락으로 잠겼다는 것이다.
다른 트랜잭션에서 다음과 같은 insert문의 실행 결과를 확인할 수 있다.
❌Insert Into tb_index(age, name) values (1, '철수');
❌Insert Into tb_index(age, name) values (2, '민수');
✅Insert Into tb_index(age, name) values (3, '영수');
이렇게 레코드 락와 갭 락을 합쳐 놓은 형태의 잠금을 넥스트 키 락이라고 부른다.
갭 락은 그 자쳅다는 이어서 설명할 넥스트 키 락의 일부로 자주 사용된다.
레코드 락과 갭 락을 합쳐 놓은 형태의 잠금을 넥스트 키 락이라고 한다.
SELECT * FROM tb_index WHERE id between 6 and 9;
해당 쿼리를 실행해보자. (tb_index 테이블의 데이터가 약간 바뀜)
PK인 id를 기준으로 봤을 때 id 4~6 사이와 6~9 사이에 빈 공간이 있다.
id가 6인 레코드와 9인 레코드에 레코드 락이 걸린다.
그리고 id 6~9 사이 간격에 갭 락이 걸리게 된다.
이것이 레코드 락 + 갭 락의 조합인 넥스트 키 락인 것이다.
갭 락 자체가 REPEATABLE READ에서 동작하기 때문에 레코드 락과 갭 락을 같이 쓰는 넥스트 키 락도 REPEATABLE READ 수준에서 동작한다.
출처: https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html
supremum psedo record는 인덱스 페이지의 끝에 있는 가상의 레코드라고 할 수 있다.
즉 락을 걸었을 때 락 정보에서 supremum psedo record를 확인했다면 락의 시작 범위 ~ 락이 걸린 인덱스 페이지의 끝 부분
에 락이 잡히는 것이다.
특정 락끼리는 공존할 수 있다.
select * from tb_index where id = 1 LOCK IN SHARE MODE;
두 개의 다른 세션에서 같은 레코드에 S-Lock을 걸었을 때.
두 세션에서 같은 레코드에 S-Lock를 걸 수 있다.
당근 기술 블로그 - MySQL Gap Lock 다시보기
https://incheol-jung.gitbook.io/docs/q-and-a/spring/feat.-tmi#acid
https://gisungcu.tistory.com/645
https://mangkyu.tistory.com/298