MySQL의 InnoDB의 경우 디폴트로 REPEATABLE READ 격리 수준을 지원한다.
REPEATABLE READ는 한 트랜잭션 내에서 동일한 데이터를 여러번 읽을 때 항상 동일한 값을 반환함을 보장하는 격리 수준이다. 이는 한 트랜잭션 내에서 첫 조회 시점에 스냅샷을 만들고 다음번에 동일한 데이터를 읽을 때는 이 스냅샷을 읽는 방식으로 구현된다.
다른 트랜잭션에서 데이터를 수정해도 내 현재 트랜잭션에선 첫 조회 시점에 만든 스냅샷을 기준으로 읽어오기 때문에, 한 트랜잭션 내에선 한번 읽은 데이터는 계속 동일한 값을 반환함을 보장한다.
이렇게 락을 사용하지 않고도 읽관된 읽기 (consistent read) 를 보장하는 동시성 제어 기법을 MVCC(Multi-Version Concurrency Control) 라고 한다. MySQL 공식 문서에는 다음과 같이 설명되어 있다.
Consistent reads within the same transaction read the snapshot established by the first read. This means that if you issue several plain (nonlocking)
SELECTstatements within the same transaction, theseSELECTstatements are consistent also with respect to each other.
그러나 주의할 점이 있다. REAPEATABLE READ 격리 단계의 경우 한 트랜잭션 내에 락을 획득하는 구문인 UPDATE, INSERT, DELETE, SELECT FOR UPDATE 을 락을 획득하지 않는 단순 SELECT 구문과 함께 사용하는 것을 지양하라고 공식 문서에 나와 있다. 이는 락을 획득하지 않는 단순 SELECT 구문의 경우 트랜잭션 내 첫 조회 시점 기준의 스냅샷을 보여주는데 반해, 락을 획득하는 구문은 스냅샷이 아닌 가장 최근의 커밋된 상태를 사용하기 때문에 두 구문이 사용하는 테이블의 일관성이 보장되지 않기 때문이다.
non-locking
SELECTstatement presents the state of the database from a read view which consists of transactions committed before the read view was created, and before the current transaction's own writes, while the locking statements use the most recent state of the database to use locking. In general, these two different table states are inconsistent with each other and difficult to parse.
REPEATABLE READ 격리 수준에서, 일반 SELECT는 트랜잭션 시작 시의 MVCC 스냅샷을 참조한다. 따라서 다른 트랜잭션이 중간에 데이터를 INSERT하고 커밋해도, 내 트랜잭션의 SELECT 결과는 항상 동일하게 유지되므로 PHANTOM READ 현상이 발생하지 않는다.
주의:
SELECT FOR UPDATE나SELECT FOR SHARE구문과 같이 락을 사용하는 경우에는 undo log를 통해 스냅샷을 조회하는게 아니라 테이블에서 직접 조회하기 때문에 PHANTOM READ 현상이 그대로 발생할 수 있다.
그러나 '데이터 확인 후 삽입(SELECT 후 INSERT)' 과 같은 쓰기 작업에서는 문제가 발생할 수 있다. SELECT는 스냅샷을 보고 "데이터 없음"이라고 판단하지만, 정작 INSERT는 스냅샷이 아닌 실제 테이블에 기록해야 한다.
이 과정에서 다른 트랜잭션에 의해 추가된 '유령' 데이터와 충돌할 수 있다.
이처럼 MVCC만으로는 쓰기 작업의 정합성을 보장할 수 없으며, 이 문제를 해결하기 위해 SELECT ... FOR UPDATE 와 같이 락을 사용해야 한다. 이는 데이터가 들어갈 Gap(간격)에 Gap Lock을 걸어, 내 트랜잭션이 끝날 때까지 다른 트랜잭션의 INSERT를 막아 데이터 정합성을 보장한다.
이때 두 가지 케이스에 따라 PHANTOM READ가 발생할 수도 있고 발생하지 않을 수 있다.
SELECT FOR UPDATE(또는 SELECT FOR SHARE) -> INSERTinnoDB의 경우 앞서 설명한 Gap Lock(정확히 말하면 Next Key Lock) 때문에 이후에 시도되는
INSERT는 락으로 인해 트랜잭션이 종료될 때까지는INSERT를 할 수 없으므로 PHANTOM READ를 방지할 수 있다.
INSERT -> SELECT FOR UPDATE(또는 SELECT FOR SHARE)
INSERT시점에 락이 걸리지 않았으므로 바로 실행된다. 따라서
만약SELECT->INSERT(다른 트랜잭션) ->SELECT FOR UPDATE순으로 쿼리가 이루어진다면 여전히 PHANTOM READ 가 발생한다.
MySQL InnoDB isolation level - 최진영님의 글
트랜잭션의 격리 수준에 대해 쉽고 완벽하게 이해하기 - 망나니 개발자님의 글
REAL MySQL 8.0 1권