요즘 CS스터디를 진행하고 있는데, 트랜잭션을 정리하고 있던 와중에 REPEATABLE READ에서 InnoDB는 RHANTOM READ가 발생하지 않는다는 정리글을 보았다.
내가 정리했던 글에서도 이를 찾아볼 수 있었는데 단순하게 글로만 표현되어 있어 나조차도 이해하기 힘들었다.
그래서 이를 좀 더 찾아보고 이게 무엇을 뜻하는지 찾아보고 정리하려고 글을 작성한다.
REPEATABLE READ
REPEATABLE READ는 동일 트랜잭션 내에서는 동일한 결과를 보장할 수 있는 격리 수준으로 MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준이다. 어떻게 동일한 결과를 보장해 줄 수 있을까? MVCC(Multi Version Concurrency Control)라는 메커니즘을 이용해 언두 영역에 이전 데이터를 백업해 두고 실제 레코드 값을 변경한다.
MVCC: 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법으로 새로운 값을 업데이트하면 이전 값은 언두(UNDO) 영역에 관리함으로써 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다. MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 데 있음.
REPEATBLE READ의 동작과정
- 사용자 B가 처음 SELECT(emp_no = 500000) 했을 때 결과로 Lara를 반환받음
- 사용자 A가 Lara를 Toto로 UPDATE 하면 언두 로그에 이전 데이터 복사
- 그 후에 사용자 B가 다시 SELECT로 읽었을 때 언두 로그를 이용해 같은 값인 Lara 반환
사용자 B가 두 번째 읽었을 때 해당 레코드의 TRX-ID(12)가 자신의 TRX-ID(10) 보다 늦게 시작되었기 때문에 언두 로그를 통해 읽었다.
PHANTOM READ(유령 읽기)
REPEATABLE READ에서 새로운 레코드의 삽입까지는 막지 않기 때문에 SELECT로 조회했을 때 다른 트랜잭션에 의해 추가된 데이터를 얻을 수 있는데 이를 PHANTOM READ라 한다.
MVCC 덕분에 일반적인 조회에서는 PHANTOM READ가 발생하지 않는다. 자신보다 나중에 실행한 트랜잭션이 추가하는 레코드는 무시하면 된다.
하지만, SELECT... FOR UPDATE 쿼리로 동일한 테이블을 조회했을 경우 달라진다. 언두 레코드에는 잠금을 걸 수 없기 때문에 잠금을 하고 조회하는 레코드의 경우 언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져오게 된다. 이로 인해 데이터의 부정합이 일어나 PHANTOM READ가 발생한다.
InnoDB 스토리지 엔진 잠금
레코드 락
- 레코드 자체만을 잠그는 것을 레코드 락이라고 한다.
- MySQL은 특이하게 레코드 자체가 아닌 인덱스의 레코드를 잠근다 - 인덱스가 하나도 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정한다.
- InnoDB의 보조 인덱스를 이용한 변경 작업은 대부분 넥스트 키락 또는 갭 락을 사용한다.
- InnoDB의 프라이머리 키 또는 유니크 인덱스에 의한 변경 작업에서는 갭에 대해서는 잠그지 않고, 레코드 자체에만 락을 건다.
갭 락
- 레코드 자체가 아닌 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미함
- 레코드와 레코드 사이 간격에 새로운 레코드가 insert 되는 것을 제어한다.
넥스트 키 락
- 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금이다.
- 바이너리 로그에 기록되는 쿼리가 레플리카 서버에서 실행될 때 소스 서버에서 만들어 낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 주목적이다.
- 넥스트 키 락과 갭 락으로 인해 데드락이 발생하거나 다른 트랜잭션을 기다리게 만드는 일이 자주 발생한다.
- 바이너리 로그 포맷을 ROW 형태로 바꿔서 넥스트 키 락, 갭 락을 줄이는 것이 좋다.
다음 4가지의 경우에 갭락이 있는 MySQL과 일반적인 RDBMS에서 어떻게 작동하는지 알아보자.
- SELECT FOR UPDATE -> INSERT -> SELECT
- SELECT FOR UPDATE -> INSERT -> SELECT FOR UPDATE
- SELECT -> INSERT -> SELECT
- SELECT -> INSERT -> SELECT FOR UPDATE
1. SELECT FOR UPDATE -> INSERT -> SELECT
일반적인 RDBMS의 경우
- 일반적인 RDBMS의 경우 갭락은 없기 때문에 2번 레코드에만 잠금이 걸림.
- 따라서 moomin3 정상적으로 INSERT
- MVCC 덕분에 User B가 두 번째 조회 시 moomin2만 나옴
-> MVCC 덕분에 PHANTOM READ 발생 X
MySQL의 경우
- MySQL의 경우 2번 레코드에 레코드 락과 id가 2보다 큰 범위에는 갭락으로 넥스트 키 락이 걸리게 된다.
- moomin3을 INSERT 하려고 하지만 갭락으로 인해 대기
- User B가 두 번째 조회했을 때 moomin2만 반환하게 됨
- User B의 트랜잭션이 커밋되고 나면 갭락이 해제되면서 User A의 트랜잭션이 진행
-> 갭락 덕분에 PHANTOM READ 발생 X
2. SELECT FOR UPDATE -> INSERT -> SELECT FOR UPDATE
일반적인 RDBMS의 경우
- 일반적인 RDBMS의 경우 갭락은 없기 때문에 2번 레코드에만 잠금이 걸림.
- moomin3 정상적으로 INSERT
- 잠금 있는 읽기는 데이터 조회가 언두 로그가 아닌 테이블에서 수행되기 때문에 두 번째 조회 시 2건이 나오게 됨.
-> 잠금 읽기 때문에 PHANTOM READ 발생
MySQL의 경우
- MySQL의 경우 2번 레코드에 레코드 락과 id가 2보다 큰 범위에는 갭락으로 넥스트 키 락이 걸리게 된다.
- moomin3을 INSERT 하려고 하지만 갭락으로 인해 대기
- 언두 로그 대신 테이블을 읽지만 어차피 아직 데이터가 Insert가 되지 않아 User B가 두 번째 조회했을 때 moomin2만 반환하게 됨
- User B의 트랜잭션이 커밋되고 나면 갭락이 해제되면서 User A의 트랜잭션이 진행
-> 갭락 덕분에 PHANTOM READ 발생 X
3. SELECT -> INSERT -> SELECT
일반적인 RDBMS와 MySQL의 결과가 같음
- User B는 아무 잠금 없이 조회.
- 잠금이 없으므로 moomin3 정상적으로 INSERT
- MVCC 덕분에 User B가 두 번째 조회 시 moomin2만 나옴
-> MVCC덕분에 PHANTOM READ 발생 X
4. SELECT -> INSERT -> SELECT FOR UPDATE
일반적인 RDBMS와 MySQL의 결과가 같음
- User B는 아무 잠금 없이 조회.
- 잠금이 없으므로 moomin3 정상적으로 INSERT
- 잠금 있는 읽기는 데이터 조회가 언두 로그가 아닌 테이블에서 수행되기 때문에 두 번째 조회 시 2건이 나오게 됨.
-> 잠금 읽기 때문에 PHANTOM READ 발생
정리
출처
RealMySQL 8.0 1권
'CS' 카테고리의 다른 글
네트워크 기본 정리 (10) | 2023.11.29 |
---|---|
인덱스 (3) | 2023.09.28 |
트랜잭션과 잠금 (0) | 2023.09.26 |
이터레이터 패턴(Iterator Pattern) (0) | 2023.05.16 |
프록시 패턴(Proxy Pattern) (0) | 2023.05.12 |