티스토리 뷰
1. READ UNCOMMITTED
READ UNCOMMITTED는 커밋되지 않은 데이터에 접근이 가능하다.
1-1. Dirty Read
READ UNCOMMITTED 격리 수준에서는 Dirty Read가 발생한다. Dirty Read는 커밋되지 않은 데이터를 다른 트랜잭션에서 읽음으로써 발생하는 문제이다. 이는 아직 데이터베이스에 반영되지 않은 변경 사항을 읽기 때문에, 이후에 변경 작업을 수행한 트랜잭션이 ROLLBACK 될 경우 문제가 발생한다.
- 트랜잭션 1이 공유 데이터 MIN을 KIM으로 UPDATE 한다.
- 아직 데이터베이스에 커밋되지 않은 상태이다.
- 이때, 트랜잭션 2는 트랜잭션 1이 커밋하지 않은 KIM 데이터를 읽는다.
- 만약 트랜잭션 1이 중간에 문제가 발생하여 ROLLBACK 할 경우, 데이터는 다시 MIN으로 돌아간다. 그러나 트랜잭션 2는 이미 잘못된 KIM 데이터를 읽었기 때문에, 데이터 불일치 문제가 발생한다.
2. READ COMMITTED
READ COMMITTED는 커밋된 데이터만 읽을 수 있는 격리 수준이다. 트랜잭션에서 쓰기 작업이 발생한 경우, 해당 트랜잭션이 커밋되기 전까지 다른 트랜잭션에서는 변경된 데이터를 읽을 수 없다.
2-1. Non-Repeatable Read
READ COMMITTED에서는 Non-Repeatable Read가 발생한다. Non-Repeatable Read는 같은 트랜잭션 내에서 동일한 데이터를 반복해서 조회할 때 다른 결과가 반환되는 문제이다.
- 트랜잭션 1이 id = 1인 데이터를 MIN에서 KIM으로 UPDATE 한다.
- 데이터베이스에 커밋되지 않은 상태이다.
- 트랜잭션 2가 id = 1인 데이터를 조회하였을 때, KIM은 아직 커밋되지 않은 데이터이므로 MIN을 읽는다.
- 이후 트랜잭션 1이 커밋되고, 트랜잭션 2가 id = 1인 데이터를 다시 조회하면 이번에는 MIN이 아닌 변경된 KIM을 조회하게 된다.
3. REPEATABLE READ
Repeatable Read는 트랜잭션이 시작된 후, 같은 데이터를 반복해서 조회할 때 항상 동일한 값을 조회할 수 있도록 보장하는 격리 수준이다. 이를 통해 Non-Repeatable Read 문제가 발생하지 않으며, MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용된다.
InnoDB는 각 트랜잭션에 고유한 트랜잭션 번호(TRX ID)를 부여한다. 이 번호는 트랜잭션이 변경 작업을 처음 수행할 때 할당된다.(트랜잭션이 시작되자마자 부여되는 것이 아님) Undo 로그에 변경되기 전의 데이터를 백업하여 필요할 때 변경 전의 데이터를 조회할 수 있도록 한다.
InnoDB의 경우, 트랜잭션은 고유한 트랜잭션 번호를 가지며, Undo 로그에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 번호가 레코드마다 같이 저장되어 있다.
- 사용자 B가 먼저 트랜잭션을 시작하고, 트랜잭션 번호 10을 할당받는다.
- emp_no = 50000인 데이터를 조회하여 (50000, JuBal)을 확인한다.
- 이후 사용자 A가 트랜잭션을 시작하고, 트랜잭션 번호 12를 할당받는다.
- emp_no = 50000인 데이터의 first_name을 Toto로 변경하는 UPDATE 문을 실행한다.
- 이때, InnoDB는 데이터의 트랜잭션 번호를 12로 변경하고, 변경 전 데이터인 (50000, JuBal)을 Undo 영역에 백업한다.
- 사용자 A가 트랜잭션을 커밋한다.
- 사용자 B는 다시 emp_no = 50000인 데이터를 조회한다.
- 이때, 조회한 데이터의 트랜잭션 번호(12)가 자신의 트랜잭션 번호(10) 보다 크므로, 해당 데이터는 처음에 조회한 데이터와 다른 변경된 데이터로 판단한다.
- InnoDB는 Undo 로그에서 사용자 B가 처음에 조회했던 데이터를 반환하여, (50000, JuBal)을 조회할 수 있도록 한다.
Repeatable Read는 트랜잭션 ID와 Undo 영역을 사용하여 Non-Repeatable Read 문제를 해결하여 동일한 데이터를 조회할 때 동일한 결과를 보장한다. 하지만 Repeatable Read는 레코드의 변경(UPDATE)에 대해서는 데이터 일관성을 보장하지만, 새로운 행의 추가(INSERT)와 삭제(DELETE)에 대해서는 보호되지 않는다.
3-1. Phantom Read
Phantom Read는 Repeatable Read 격리 수준에서 발생하는 문제이다. Repeatable Read는 트랜잭션 도중 데이터의 일관성을 유지하기 위해 트랜잭션 ID와 Undo 로그를 사용하여 Non-Repeatable Read 문제를 방지한다.
그러나 Repeatable Read는 UPDATE에 대한 데이터 정합성은 보장하지만, INSERT나 DELETE로 인해 발생하는 데이터 변경에 대해서는 일관성을 보장하지 않는다. 이러한 문제를 Phantom Read라고 한다.
- Bob은 post_id = 1인 레코드를 조회하는 트랜잭션을 시작한다. 이때, post_comment 테이블에서 세 개의 레코드가 조회된다.
- 이후 Alice가 트랜잭션을 시작하고 post_id = 1인 새로운 레코드를 삽입한다.
- Bob은 다시 post_id = 1인 레코드를 조회할 때, 처음에는 세 개의 레코드가 조회되었으나 이번에는 네 개의 레코드가 조회되었다. 즉, Bob 트랜잭션이 실행되는 중간에 Alice 트랜잭션이 새로운 데이터를 추가했기 때문에 Phantom Read가 발생한다.
따라서 Repeatable Read 격리 수준에서도 데이터 부정합 문제가 발생할 수 있다.
3-2. Phantom Read 방지 방법
MySQL의 InnoDB 스토리지 엔진은 Next-Key Lock을 사용하여 Phantom Read 문제를 해결할 수 있다. Next-Key Lock은 조회한 레코드뿐만 아니라 레코드 사이의 갭까지 Lock을 걸어 새로운 레코드가 삽입되는 것을 방지한다. 이를 통해 Repeatable Read 격리 수준에서도 Phantom Read를 방지할 수 있다.
📌 Next-Key Lock이란? → https://github.com/heemanglee/learn-repository/discussions/29
4. SERIALIZABLE
SERIALIZABLE는 가장 높은 격리 수준으로, 트랜잭션 간에 발생할 수 있는 모든 문제를 해결한다. 이 격리 수준에서는 모든 트랜잭션이 직렬적(serialized)으로 처리되는 것처럼 작동하며, Dirty Read, Non-Repeatable Read 그리고 Phantom Read 문제가 발생하지 않도록 보장한다.
5. Shared Lock(공유 잠금)
SERIALIZABLE 격리 수준에서는 트랜잭션이 SELECT 문을 수행할 때, 해당 데이터에 Shared Lock(공유 잠금)을 설정한다. Shared Lock은 여러 트랜잭션이 동시에 해당 데이터를 SELECT 할 수 있도록 허용하지만, 쓰기 작업은 모두 차단한다. 즉, 데이터가 Shared Lock으로 잠겨 있는 동안은 다른 트랜잭션은 그 데이터를 수정하거나 삭제할 수 없다.
6. Exclusive Lock(배타적 잠금)
쓰기 작업을 하기 위해서는 Exclusive Lock(배타적 잠금)이 필요하다. Exclusive Lock은 해당 데이터에 대해 하나의 트랜잭션만 접근할 수 있도록 보장한다. Exclusive Lock이 걸린 데이터는 다른 트랜잭션에서 읽기 및 쓰기 작업 모두 차단되며, 트랜잭션이 Exclusive Lock을걸기 위해서는 해당 데이터에 설정된 모든 Shared Lock이 해제되어야 한다.
7. 동작 방식 그리고 성능 저하
SERIALIZABLE 격리 수준은 트랜잭션이 데이터를 읽는 동안 해당 데이터에 대해 다른 트랜잭션이 쓰기 작업을 할 수 없기 때문에, 데이터의 정합성이 철저하게 보장된다.
그러나 철저한 데이터 일관성 보장 때문에, 트랜잭션 간의 동시 처리 성능이 저하될 수 있다. 그 이유는 트랜잭션이 데이터를 읽고 있는 동안 다른 트랜잭션이 해당 데이터에 접근할 수 없기 때문이다. 이는 쓰기 작업이 빈번하게 발생하는 환경에서는 더 큰 성능 저하로 이어진다.