트랜잭션 격리 수준이란, 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지 결정하는 것을 의미한다.
총 네 단계로 나뉘며, 뒤로 갈수록 고립 정도(격리)가 높아져 동시 처리 성능이 떨어져서 잘 사용하지 않는다. 따라서 MySQL
은 주로 REPETABLE READ
를 사용하며, 오라클은 READ COMMITED
혹은 REPETABLE READ
를 사용한다.
ReadView
란, MVCC
를 지원하기 위해서 사용되는 값으로 다음의 네 가지 정보를 가지고 있으며 스냅샷(SnapShot)이라고도 불린다.
creator_trx_id
: 이 ReadView
를 만든 트랜잭션 IDtrx_ids
: ReadView
가 생성되는 시점의 활성화 되어 있는 트랜잭션들up_limit_id
: 활성화 된 트랜잭션 중 가장 오래된 트랜잭션을 의미한다. 해당 값보다 트랜잭션 ID
가 큰 레코드는 읽지 않고 작은 데이터만 읽는다.low_limit_id
: 활성화 된 트랜잭션 중 가장 최신 트랜잭션을 의미한다. 해당 값보다 트랜잭션 ID
가 작은 데이터는 모두 읽는다.Read Committed
격리 수준에서는 매 쿼리가 실행될 때 마다 ReadView
를 생성하지만,Repeatable Read
수준에서는 트랜잭션이 실행할 때 한번만 ReadView
를 생성한다.
스냅샷 개념을 알고 넘어가야 하는게, 각 수준에서 ReadView
정보를 활용해서 현재 트랜잭션에서 어떤 데이터를 읽을 수 있을지 판단하기 때문이다.
가장 격리 수준이 낮은 단계로, 각 트랜잭션에서의 변경 내용을 커밋이나 롤백 여부에 관계 없이 다른 트랜잭션에서 조회할 수 있다.
하지만 이렇게 되면 치명적인 문제가 발생한다. 해당 트랜잭션에서 예기치 못한 문제가 발생해 롤백을 하게 되면, 다른 트랜잭션 입장에서는 해당 데이터가 존재한다고 생각하고 계속 진행을 할 수 도 있기 때문이다. (데이터가 나타났다가 사라졌다가 하는 현상을 초래한다.)
이처럼 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 조회할 수 있는 현상을 Dirty-Read
라고 한다. 데이터 정합성을 보장해주지 못한다.
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
어떤 트랜잭션에서 데이터를 변경했더라도, 커밋이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있다. 이를 위해 커밋이 완료되기 이전까진, 조회 시 언두 로그에 백업해놓은 원본 레코드를 활용해 변경 이전 초기 데이터를 보여준다.
트랜잭션 내에서 매 쿼리가 시작될 때 마다 Read View
가 생성된다. 그리고 쿼리로 조회한 레코드를 읽었을 때, 트랜잭션 ID
를 확인한다.
당연히 조회하려는 레코드의 트랜잭션 ID
가 현재 활성화 된 트랜잭션인 경우, 커밋되지 않은 값이므로 읽지 않고 넘어간다.
하지만 트랜잭션
ID
가 현재 활성화 되지 않은 경우라면?
TRX-ID
값보다 작은 경우의 레코드도 역시 커밋되었으므로 읽는다.이렇게 ReadView
가 가지고 있는 활성 트랜잭션 ID
에 해당하지 않는 레코드라면 커밋된 값으로 인식하고 모두 읽어버리기 때문에 읽기 일관성이 깨질 수 있다. 설령 그게 나중에 생성되었으나 먼저 커밋한 트랜잭션임에도 불구하고 말이다.
더티 리드와 같은 현상은 발생하지 않으며 가장 많이 선택되는 격리 수준이다.
하나의 트랜잭션 내에서 같은 쿼리에 대해 다른 조회 결과가 반환될 수 있다.따라서 해당 레벨에서는 트랜잭션 내에서 실행되는 SELECT
나, 트랜잭션이 아예 없이 실행되는 SELECT
나 차이가 존재하지 않는다. 이를 NON-REPEATABLE READ
문제라고 한다.
예를 들어 트랜잭션을 하루 단위로 시작하고 하루에 입금된 금액의 총합을 구해야 하는 상황을 가정해보자. 이때 REPEATABLE READ
가 보장이 안된다면 총합을 계산하는 조회 쿼리는 실행시마다 다른 결과를 가져오게 된다.
중요한 것은, 우리가 실행하는 SQL 문장이 어떤 결과를 가져오게 되는지 정확히 예측하지 못한다는 것이다.
SELECT 문장도 트랜잭션 범위 내에서 작동한다. 즉, 트랜잭션 내부에서는 다른 트랜잭션에서 커밋을 하더라도 같은 쿼리에 대해 항상 동일한 결과만 볼 수 있다.
언두 영역에 데이터를 백업할 때는, 변경을 발생시킨 트랜잭션 번호를 같이 기록한다. (고유하다) 이러한 정보를 이용해 SELECT
문장이 일어났던 트랜잭션 번호보다 작은 트랜잭션 번호를 가지고 있는 언두 로그 데이터만 데이터만 조회할 수 있다.
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT count(*) FROM employees WHERE emp_no > 400000; // (1)
COMMIT;
SELECT count(*) FROM employees WHERE emp_no > 400000; // (2)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
INSERT INTO `employees` VALUES (430639,'1960-03-24','Piyush','Leaver','F','1990-09-07');
1번 시점에서 조회를 했을 때는 트랜잭션이 종료되지 않았으므로, 새로운 레코드가 삽입되어도 emp_no
가 400000
이 넘는 사람은 없다고 조회된다.
이후 2번 시점에서 트랜잭션이 종료가 되면, 성공적으로 반영된 데이터를 읽어온다.
NON_REPEATABLE READ
현상이 일어나지 않는다.
일부 쿼리 중 SELECT .. FOR UPDATE
나 SELECT ... LOCK IN SHARE MODE
와 같이 베타적/공유 락을 얻으려고 시도하는 조회 쿼리에 대해서는, 데이터의 부정합이 발생할 수 있다.
애초에 해당 쿼리를 날리는 시점에 해당 쿼리는 언두 영역의 변경 전 데이터를 가져오는게 아니라, 현재 가장 최신의 레코드의 값을 가져오는것에 목적이 있기 때문이다.
🔖 SELECT .. FOR UPDATE
데이터 수정하려고SELECT
하는 중일때, 다른 사람들은 데이터를 수정하지 못하게 막을 수 있다. 즉 동시성 제어를 위하여 특정 데이터(ROW)에 대해 베타적 락을 거는 쿼리이다. (언두 레코드에는 잠금을 걸 수 없다.)
https://dololak.tistory.com/446
이렇게 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 생겼다 사라졌다 하는 현상을 Phantom Read
라고 한다.
SELECT
트랜잭션이 시작된 이후로는 다른 트랜잭션에서 UPDATE
데이터는 볼 수 없지만, INSERT
된 데이터는 볼 수 있다. 따라서, 다음과 같이 동일한 쿼리에 대해 반환 데이터의 개수가 달라질 수 있다는 문제가 발생한다.
MySQL
에서는Phantom Read
현상이 일어나지 않는다!
MySQL8.0 버전에서는 InnoDB
스토리지 엔진이 기본값이다.
이렇기에 InnoDB
에서 걸 수 있는 잠금인 갭 락(Gap Lock)과 넥스트 키 락(Next Key Lock)를 활용하여, Phantom Read
현상을 방지할 수 있다. 해당 락이 조회된 범위 사이에 잠금을 걸어 새로운 레코드가 추가되는 것을 막아주기 때문이다.
따라서 아래와 같이 새로운 레코드를 추가하려 하면 락으로 인해 대기하게 된다.
가장 엄격한 격리 수준으로, 동시 처리 성능이 떨어진다.
InnoDB
테이블에서 기본적으로 순수한 SELECT는 잠금 없이도 일관되게 읽을 수 있도록 하는데, SERIABLIZABLE
레벨을 선택하면 조회 작업조차도 공유 잠금을 획득해야 한다.
따라서 어짜피 앞서서 말했듯이 팬텀 리드 현상이 일어나지 않으니, REPEATABLE READ
를 사용하면 되기 때문에 굳이 SERIABLIZABLE
을 사용할 일은 없어보인다.
https://tecoble.techcourse.co.kr/post/2022-11-07-mysql-isolation/