DB - 트랜잭션 잠금 수준
데이터베이스 스터디 5주차에서 학습하고 정리한 내용입니다.
1. 트랜잭션 격리 수준
여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정한다. 격리 수준은 크게 **READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE로 나뉜다. SERIALIZABLE로 갈 수록 데이터 부정합은 제거되지만 동시성을 활용할 수 없다. 따라서 일반적인 데이터베이스는 READ COMMITTED나 REPEATABLE READ를 많이 사용한다. Oracle은 주로 READ COMITTED 수준을 많이 사용하며 MySQL은 REPEATABLE READ를 주로 사용한다.
DIRTY READ | NON-REPEATABLE READ | PHANTOM READ | |
---|---|---|---|
READ UNCOMMITTED | O(발생) | O | O |
READ COMMITTED | X(없음) | O | O |
REPEATABLE READ | X | X | O (InnoDB는 X) |
SERIALIZABLE | X | X | X |
2. READ UNCOMMITTED
READ UNCOMMITTED 격리 수준에서는 각 트랜잭션에서의 변경 내용이 커밋이나 롤백 여부에 상관없이 다른 트랜잭션에서 보인다.
위 상황에서 사용자 A가 INSERT(Lara)를 롤백하면 문제가 발생한다. 데이터베이스에서는 반영이 되지 않았지만 사용자 B의 입장에서는 Lara가 정상적인 사원이라 가정하고 계속 처리하게 된다. B가 트랜잭션 종료 후 다음 트랜잭션에서 Lara를 조회할 때 Lara가 데이터베이스에 없으므로 B 입장에서는 데이터가 사라진 것으로 보인다. 이렇게 트랜잭션의 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있어 데이터가 나타났다가 사라졌다 하는 현상을 DIRTY READ라고 한다.
이 격리 수준에서는 트랜잭션이 다른 트랜잭션의 부분 업데이트된 데이터를 읽어 올 수 있으므로 트랜잭션의 일관성이 보장되지 않을 수 있다. 이 경우 데이터 일관성을 유지하기 위해 많은 추가 비용이 발생할 수 있으므로 극단적으로 성능이 중요한 상황이 아니면 잘 사용되지 않는 격리 수준이다.
3. READ COMMITTED
READ COMMITED 격리 수준에서는 각 트랜잭션이 커밋한 내용만 다른 트랜잭션이 확인할 수 있다. 따라서 READ UNCOMMITTED 격리 수준에서 등장하는 DIRTY READ가 나타나지 않는다.
사용자 A가 이름을 Lara에서 Toto로 변경하면 테이블에는 ToTo로 변경된다. 하지만 Lara의 정보는 제거되지 않고 언두 로그(Undo Log)라는 별도의 공간에 기록된다. 이 언두 로그는 트랜잭션이 롤백 될 때 필요한 정보를 가지고 있기 위함인데 READ COMMITTED에서는 A와 B가 접근하는 데이터가 달라진다.
- 트랜잭션을 생성하여 쓰기 작업을 수행한 A는 작업한 테이블에 접근하여 변경된 데이터를 가져올 수 있다.
- A외 쓰기 작업을 수행하지 않은 트랜잭션들은 테이블에 접근할 수 없고 언두 로그에만 접근할 수 있다.
- A가 트랜잭션을 커밋하면 모든 트랜잭션이 언두 로그가 아닌 테이블을 참조한다.
이렇게 트랜잭션이 커밋, 롤백되기 전까지 다른 트랜잭션이 변경된 값을 참조하지 못하게 함으로써 DIRTY READ 문제를 해결할 수 있다. 하지만 동일한 데이터를 두번 읽는 상황에서 여전히 데이터가 나타났다 사라지는 NON-REPEATABLE READ가 발생할 수 있다.
- B가 트랜잭션을 실행해서 emp_no 500000을 조회하여 Lara 값을 얻는다.(트랜잭션 종료하지 않는다.)
- A가 emp_no 500000의 first_name을 Toto로 변경하여 커밋한다.
- B가 다시 emp_no 500000의 first_name을 조회하면 Lara가 아닌 Toto 값을 얻는다.
한 트랜잭션의 내에서 동일한 데이터를 두 번 읽을 때 값이 일치하지 않게 된다. 이 경우 하나의 트랜잭션에서 똑같은 SELECT 쿼리를 실행했을 때는 항상 같은 결과를 가져와야 한다는 REPEATABLE READ 정합성에 어긋날 수 있다.
4. REPEATABLE READ
REPEATABLE READ 격리 수준에서는 READ COMMITTED 격리 수준에서 발생하는 NON-REPEATABLE READ 문제가 발생하지 않는다. READ COMMITTED와 비슷하게 언두 로그에 이전 버전의 데이터를 저장하고 있으며 어느 시점의 데이터를 가져올 지에 대한 차이가 있다.
모든 트랜잭션은 생성된 순서에 따라 트랜잭션 ID가 존재한다. READ COMMITTED는 가장 최근에 커밋된 데이터를 가져오지만 REPEATABLE READ는 자신(트랜잭션)의 생성 시점 이후의 변경 사항은 읽어오지 않는다.
REPEATABLE READ는 먼저 레코드의 트랜잭션 ID 값을 읽은 후 어떤 값을 가져올 지 판단한다.
- 레코드의 트랜잭션 ID 값이 나(트랜잭션)의 트랜잭션 값보다 뒤라면 테이블이 아닌 언두 로그를 참조한다.
- 레코드의 트랜잭션 ID 값이 나의 트랜잭션 값보다 앞이라면 테이블의 값을 읽어온다.
이 언두 로그에는 하나의 레코드에 백업이 여러 개 존재할 수 있다. 만약에 트랜잭션이 시작하고 종료되지 않거나 트랜잭션의 크기가 크다면 언두 영역에 백업된 데이터의 크기도 커지게 된다. 이렇게 되면 MySQL 서버의 처리 성능이 떨어질 수 있다.
팬텀 읽기
이 REPEATABLE READ는 변경 사항에 대한 데이터 불일치는 해결할 수 있지만 데이터 생성으로 발생하는 데이터 불일치는 해결할 수 없다.
하지만 이 그림을 보고 의문이 들었다. 새로 추가된 데이터는 나(트랜잭션)의 번호보다 뒤니까 삽입된 레코드 또한 트랜잭션 ID를 읽고 가져오지 않으면 팬텀 읽기는 해결되지 않을까? 이와 관련한 답을 여기에서 찾을 수 있었다.
이를 정리해보자면
- ANSI SQL(표준 SQL)에서는 REPEATABLE READ에서 팬텀 읽기가 발생할 수 있다.
- ANSI SQL에서 확장된 일부 데이터베이스(MySQL InnoDB, PostgreSQL 등)는 MVCC를 지원하여 단순 SELECT 시 팬텀 읽기를 방지할 수 있다.
- 하지만 배타적 잠금을 사용한 조회 시는 MVCC를 활용하지 않고 최신 데이터를 기반으로 읽기 때문에 팬텀 읽기가 발생할 수 있다.
- (또) 그렇지만 MySQL은 배타적 잠금을 사용한 조회 시 범위 내 INSERT를 막는 락이 존재하므로 배타적 잠금을 사용한 조회 시에도 팬텀 읽기를 방지할 수 있다.
2) MVCC로 팬텀 읽기를 방지할 수 있는 이유
MVCC를 지원하는 데이터베이스는 특정 시점의 데이터 상태를 스냅샷으로 저장할 수 있다. 따라서 MVCC를 지원하는 데이터베이스는 트랜잭션 시작 당시의 스냅샷을 저장하여 이후 삽입되거나 수정되는 트랜잭션 ID를 조회하지 않고 시작 당시의 스냅샷 데이터만을 가져와 팬텀 읽기를 방지할 수 있다.
3) 배타적 잠금을 사용한 조회 시 팬텀 읽기가 발생하는 이유
배타적 잠금을 사용한 조회 시 MVCC를 사용하지 않고 테이블의 최신 데이터 상태를 기반으로 읽기 때문에 팬텀 읽기가 발생할 수 있다. 이 경우, 스냅샷을 활용할 수 없고 기존 레코드에 대한 잠금만 설정되며 새로운 데이터의 삽입을 막지는 못하기 때문에 팬텀 읽기가 발생할 수 있다.
스냅샷을 읽을 때 활용하는 언두 로그는 삽입만 가능한 Append-Only 구조이다. Append-Only 구조는 변경 사항이 있을 때 기존의 데이터를 수정하는 것이 아니라 변경 전 상태를 삭제하지 않고 변경된 상태를 새로 저장한다. 언두 로그의 레코드는 수정을 할 수 없으므로 배타적 잠금을 걸 수 없다.
따라서 배타적 잠금을 사용한 조회 시 언두 로그가 아닌 최신 데이터 테이블의 레코드를 잠그고 여기에서 읽기 때문에 팬텀 읽기가 발생하는 것이다.
4) MySQL이 배타적 잠금을 사용한 조회 시 팬텀 읽기를 막는 방법
MySQL은 새로운 데이터 삽입에 대한 잠금까지 제공하므로 팬텀 읽기가 발생하지 않는다. 트랜잭션 내에서 특정 범위의 데이터를 조회할 때 MySQL InnoDB는 트랜잭션이 진행 중일 때 이 범위 내에 다른 트랜잭션이 새로운 데이터를 삽입하지 못하도록 막는 추가적인 락이 존재한다. 이를 갭 락(Gap Lock)이라고 한다.
4. SERIALIZABLE
팬텀 읽기 현상을 막기 위해 SELECT 쿼리 모두 공유 락을 자동으로 걸어 읽기 작업 시 다른 트랜잭션에서 데이터를 수정할 수 없다. 하지만 이 경우는 동시 처리 성능을 거의 포기하다시피 해야할 단계이기 때문에 잘 사용하지 않는다. 위와 같이 팬텀 읽기 방지는 보통 SERIALIZABLE 대신 REPEATABLE READ에서 필요할 때 갭 락을 걸거나 MVCC를 활용하는 더 효율적인 방법을 사용한다.
Reference
https://mangkyu.tistory.com/299
백은빈, 이성욱 편저, Real MySQL 8.0, 위키북스