데이터베이스 트랜잭션에서 격리 수준은 여러 트랜잭션이 동시에 실행될 때 각각의 트랜잭션이 다른 트랜잭션의 중간 결과에 얼마나 영향을 받을지 결정하는 중요한 설정이다.
이를 통해 데이터의 일관성을 유지하고 잠재적인 동시성 문제를 방지할 수 있다.
예를 들어, 두 개 이상의 트랜잭션이 동시에 동일한 데이터를 읽거나 쓰려 할 때 발생할 수 있는 문제를 격리 수준으로 조정하여, 각 트랜잭션이 수행되는 동안 다른 트랜잭션의 영향을 받지 않도록 한다.
낮은 수준에서 높은 수준으로 데이터 일관성은 증가하지만 성능이 낮아질 수 있다.
가장 낮은 수준의 격리 수준으로 커밋되지 않은 데이터도 읽을 수 있다.
Dirty Read가 허용되며 트랜잭션이 완료되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다.
성능이 높지만 데이터 일관성이 떨어진다.
예시 : 트랜잭션 B가 balance를 1000에서 500으로 변경 중일 때 트랜잭션 A가 이를 읽으면 500으로 조회하게 된다. 이후 트랜잭션 B가 롤백되면 트랜잭션 A는 잘못된 데이터를 읽은 것이 된다.
문제점 : Dirty Read, Non-repeatable Read, Phantom Read 발생 가능.
커밋된 데이터만 읽을 수 있다.
Dirty Read는 방지되지만 Non-repeatable Read와 Phantom Read는 발생할 수 있다.
예시: 트랜잭션 A가 특정 계좌의 balance를 1000으로 조회한 후 트랜잭션 B가 balance를 500으로 변경하고 커밋한다. 트랜잭션 A가 다시 조회하면 balance가 500으로 바뀌어 있다.
문제점 : Non-repeatable Read, Phantom Read 발생 가능.
트랜잭션이 시작된 이후 조회한 데이터가 변경되지 않도록 보장한다.
MVCC와 Undo 로그를 사용하여 트랜잭션 시작 시의 데이터 스냅샷을 유지하고 이를 통해 다른 트랜잭션이 데이터를 수정하더라도 트랜잭션 내에서는 언제나 동일한 데이터를 조회할 수 있다.
MVCC와 Undo 로그의 역할
MVCC : 트랜잭션 시작 시점의 데이터 스냅샷을 생성하여 일관된 조회를 보장한다.
Undo 로그 : 데이터 변경 전의 상태를 기록하여 데이터 복구와 일관된 조회를 지원한다.
변경 전 데이터 상태 저장: 트랜잭션이 데이터를 수정할 때 Undo 로그는 수정 전의 데이터를 기록한다. 이를 통해 다른 트랜잭션이 이전 버전을 조회하여 트랜잭션 시작 시점의 일관성을 유지할 수 있다.
스냅샷 조회 시 버전 제공: 트랜잭션이 스냅샷 읽기를 수행할 때 Undo 로그에 저장된 이전 데이터 버전을 참조하여 해당 트랜잭션 시작 시점의 상태를 제공한다.
불변의 데이터 버전 관리: Undo 로그에 기록된 이전 데이터 버전은 다른 트랜잭션이 조회할 때 항상 동일하게 유지되어 시작 시점의 일관된 조회가 가능하도록 한다.
Dirty Read와 Non-repeatable Read를 방지할 수 있지만 Phantom Read는 여전히 발생할 수 있다.
예시 : 트랜잭션 A가 balance를 1000으로 조회하고, 트랜잭션 B가 balance를 500으로 변경한 후 커밋해도, 트랜잭션 A는 계속해서 balance를 1000으로 읽게 된다. 그러나 트랜잭션 B가 새로운 레코드를 삽입하면 이 새로운 레코드는 트랜잭션 A에서 조회 시 포함될 수 있어 Phantom Read가 발생할 수 있다.
Phantom Read가 발생하는 이유
MySQL의 InnoDB 엔진은 Repeatable Read 격리 수준에서 Phantom Read 문제를 방지하기 위해 다음과 같은 잠금 메커니즘을 사용한다.
레코드 락: 특정 레코드가 명확히 지정될 때 해당 레코드의 수정과 삭제를 방지한다.
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;갭 락: 레코드 사이의 공백(갭)에 잠금을 설정하여 새로운 레코드의 삽입을 방지하는 데 사용된다. 이 잠금은 읽기 잠금과 쓰기 잠금으로 설정할 수 있으며 두 가지 방식에 따라 다르게 동작한다.
SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 LOCK IN SHARE MODE;SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 FOR UPDATE;넥스트 키 락: 레코드 락과 갭 락을 결합한 잠금 방식으로 조회된 레코드와 그 앞뒤 갭에 걸쳐 잠금을 설정하여 레코드의 수정, 삭제 및 갭 내 삽입을 모두 방지한다.
SELECT * FROM accounts WHERE balance >= 300 FOR UPDATE;따라서 InnoDB의 Repeatable Read 격리 수준에서는 넥스트 키 락을 사용해 조회 범위 내 레코드의 삽입, 삭제, 수정을 모두 막음으로써 Phantom Read 문제를 효과적으로 방지한다.
| 잠금 종류 | 설명 | 사용 목적 | 예시 | 적용 상황 |
|---|---|---|---|---|
| 레코드 락 | 특정 레코드의 인덱스 항목에 락을 걸어 해당 레코드 수정과 삭제 방지 | 특정 레코드를 명확히 지정하고 독점 잠금을 설정하여 일관성 유지 | SELECT * FROM accounts WHERE id = 1 FOR UPDATE; | 인덱스 컬럼을 기준으로 명확히 특정 레코드를 지정할 때 해당 인덱스 항목에 락을 설정. |
| 갭 락 | 레코드 사이의 공백 구간(갭)에 잠금을 걸어 새로운 레코드의 삽입 방지 | 범위 내 새로운 레코드 삽입을 방지하여 트랜잭션 간 일관성 유지 | SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 LOCK IN SHARE MODE; | 범위 내에 레코드가 없을 경우 공백 구간에만 락을 걸어 삽입 방지. 조회는 가능. |
| 넥스트 키 락 | 레코드 락과 갭 락을 결합하여 레코드와 그 주변 갭 전체에 잠금 설정 | 조회 범위 전체의 삽입, 수정, 삭제를 막아 Phantom Read 방지 | SELECT * FROM accounts WHERE balance >= 300 FOR UPDATE; | 범위 내 레코드가 존재하는 경우, 레코드와 그 앞뒤 갭에 대해 전체적으로 잠금이 설정되어 삽입, 수정, 삭제가 방지. |
가장 높은 격리 수준으로 모든 트랜잭션이 순차적으로 직렬화된 것처럼 실행되는 것을 보장하여 동시성 문제를 완벽하게 방지할 수 있다.
트랜잭션 간 동시성 문제를 완전히 방지하지만 모든 트랜잭션을 직렬화하여 처리해야 하기 때문에 동시성 성능이 가장 낮은 격리 수준이다.
넥스트 키 락을 사용하여 트랜잭션 간 데이터 일관성을 유지하지만 이러한 잠금 방식은 성능 저하를 유발할 가능성이 높다.
동시성 문제가 발생하지 않는 이유
예시 : 트랜잭션 A가 balance가 1000 이상인 레코드를 조회하고 있는 동안 트랜잭션 B는 balance 값에 대해 수정하려고 시도한다. 그러나 트랜잭션 A가 종료되고 커밋될 때까지 트랜잭션 B는 대기해야 하고 수정 작업을 진행할 수 없다.
문제점: 성능 저하 가능성 높음.
| 격리 수준 | Dirty Read 발생 | Non-repeatable Read 발생 | Phantom Read 발생 | 성능 |
|---|---|---|---|---|
| Read Uncommitted | 가능 | 가능 | 가능 | 매우 높음 |
| Read Committed | 불가능 | 가능 | 가능 | 높음 |
| Repeatable Read | 불가능 | 불가능 | 가능(mysql은 불가능) | 보통 |
| Serializable | 불가능 | 불가능 | 불가능 | 낮음 |