트랙잭션은 ACID의 Isolcation에 따라 서로 간섭하지 않고 독립적으로 동작한다. 때문에 동시에 여러 트랜잭션이 진행될 때 트랜잭션의 작업 결과를 여타 트랜잭션에게 어떻게 노출한것인지를 격리 수준을 통해 결정해야한다.
다음과 같은 격리수준이 있다.
- READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
- SERIALIZABLE
READ_UNCOMMITTED
커밋하지 않은 데이터조차 접근할 수 있는 격리수준으로 다른 트랜잭션의 작업이 커밋 또는 롤백 되지 않아도 변경되는 즉시 접근할 수 있다.
때문에 다음과 같은 현상이 발생한다.
사용자 A의 트랜잭션에서 테이블에 id가 51인 멤버데이터를 삽입한상태이고 아직 커밋하지 않은 상태이지만 사용자 B는 해당 데이터에 접근이 가능하여 id가 51인 멤버 데이터를 얻고자 하면 얻을 수 있다.
이때 사용자 A의 트랜잭션이 커밋되지 않고 롤백되었다.
사용자 B의 트랜잭션이 id 가 51인 데이터를 처리하고 있는 상황이라면 다시 데이터를 조회할 시 결과를 얻을 수 없다.
위와같은 상황을 Dirty Read 라고하며 시스템에 심각한 오류를 발생시킨다. 그래서 READ UNCOMMITED는 RDBMS 표준에서 인정하지 않을 정도로 정합성에 문제가 많은 격리 수준이다.
READ_COMMITTED
커밋된 데이터만 조회할 수 있는 격리수준이다. READ_COMMITTED격리 수준에서는 다음과 같은 상황이 발생할 수 있다.
사용자 A가 id가 50번인 멤버의 name을 Mangkyu에서 MinKyu로 변경하고 아직 커밋하지 않은 상황이다.
이때 사용자 B가 id가 50인 멤버 데이터에 접근한다면 아직 사용자A의 트랜잭션이 커밋되지 않았으므로 언두로그의 데이터로 접근하여 바뀌기 전 name인 MangKyu를 얻는다. 이로써 Dirty Read현상을 방지 할 수 있다.
하지만 위와 같은 상황에서 사용자 A의 트랜잭션이 커밋되기 전에는 사용자 B가 name이 Minkyu인 멤버 데이터를 조회시 아무것도 반환되지 않는다. 하지만 사용자 A의 트랜잭션이 커밋되고 난 후 사용자 B가 같은 쿼리를 통해 데이터 조회시 결과를 얻을 수 있다. 이러한 현상을 Non-Repeatable Read(반복 읽기 불가능)이라 한다.
Non-Repeatable Read는 일반적인 경우에는 크게 문제가 되지 않지만 하나의 트랜잭션에서 동일한 데이터를 여러번 읽고 변경하는 작업이 금전적인 처리와 연결되면 문제가 생길 수 있다.
REPEATABLE_READ
RDBMS는 변경 전의 레코드를 언두 공간에 백업해준다. 그러면 변경 전/후 데이터가 모두 존재하므로, 동일한 레코드에 대해 여러 버전의 데이터가 존재한다고 하여 이를 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어) 라 한다.
MVCC를 통해 트랜잭션이 롤백된 경우에 데이터를 복원할 수 있을 뿐 아니라 서로 다른 트랜잭션간에 접근할 수 있는 데이터를 세밀하게 제어할 수 있다.
각각의 트랜잭션은 순자 증가하는 고유한 트랜잭션 번호가 존재하며, 백업 레코드에는 어느 트랜잭션에 의해 백업되었는지 트랜잭션 번호를 함께 저장한다.
다음과 같은 상황은 사용자 B의 트랜잭션이 사용자 A의 트랜잭션 보다 먼저 시작된 트랜잭션이다. 이때 REPEATABLE READ는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고해서 데이터를 조회한다.
때문에 위 상황에서는 사용자 B의 트랜잭션이 사용자 A의 트랜잭션보다 먼저 실행되었기 때문에 커밋된 테이블을 조회하는 것이 아닌 언두로그를 참고하여 데이터를 조회한다.
때문에 REPEATABLE READ는 Non-Repeatable Read 현상을 방지할 수 있다.
하지만 REPEATABLE READ는 새로운 레코드의 추가까지는 막지 못한다. 이를 Phantom Read라 하는데 SELECT로 조회한 경우 트랜잭션이 끝나기 전에 다른 트랜잭션에 의해 추가된 레코드가 발견되는 현상이다.
하지만 MVCC덕분에 일반적인 조회에서 Phantom Read는 발생하지 않는다.
하지만 잠금이 사용되는 경우에는 Phantom Read가 발생할 수 있다. 일반적인 다른 RDBMS는 다음과 같은 상황에서 Phantom Read가 발생한다.
사용자 B가 먼저 데이터를 조회하는데 SELECT FOR UPDATE를 이용하여 쓰기 잠금을 걸었다. 락은 트랜잭연이 커밋 또는 롤백될 때 해제된다.
이때 일반적인 RDBMS는 갭락이 존재하지 않으므로 id가 50인 레코드만 잠금이 걸린 상태이기 때문에 사용자 B가 동일한 쓰기 잠금 쿼리로 다시한번 데이터를 조회하면 이번에는 2건의 데이터가 조회된다. 동일한 트랜잭션 내에서도 새로운 레코드가 추가되는 경우에는 조회 결과가 달라진다.
하지만 MySQL에는 갭락이 존재하기 때문에 위의 상황에서 문제가 발생하지 않는다.
사용자 B가 SELECT FOR UPDATE로 데이터를 조회한 경우 id가 50인 레코드에는 레코드 락, id가 50보다 큰 범위에는 갭 락으로 넥스트 키락을 건다. 따라서 사용자 A가 id = 51인 멤버를 삽입시도 한다면 B의 트랜잭션이 종료 될때 까지 기다린뒤 커밋한다. 때문에 사용자 B는 새로운 레코드가 추가되어도 동일한 트랜잭션 내에서 같은 결과값을 얻을 수 있다.
SERIALIZABLE
SERIALIZABLE은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시킨다. SERIALIZABLE에서 여러 트랜잭션이 동일한 레코드에 동시 접근할 수 없으므로, 어떠한 데이터 부정합 문제도 발생하지 않는다. 하지만 트랜잭션이 순차적으로 처리되어야 하므로 동시 처리 성능이 매우 떨어진다. 따라서 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용해선 안된다.
격리 수준과 발생할 수 있는 현상을 표로 정리하면 다음과 같다.
Dirty Read | Non-Repeatable Read | Phantom Read | |
READ UNCOMMITED | 발생 | 발생 | 발생 |
READ COMMITTED | 없음 | 발생 | 발생 |
REPEATABLE READ | 없음 | 없음 | 발생(MySQL은 거의 없음) |
SERIALIZABLE | 없음 | 없음 | 없음 |
격리 수준이 높아질수록 MySQL 서버의 처리 성능이 많이 떨어질 것으로 생각하는데, 사실 SERIALIZABLE이 아니라면 크게 성능 개선 및 저하는 발생하지 않는다. 그 이유는 결국 언두 로그를 통해 레코드를 참조하는 과정이 거의 동일하기 때문이다. 따라서 MySQL은 갭 락을 통해 Phantom Read까지 거의 발생하지 않고, READ COMMITTED보다는 동시 처리 성능은 뛰어난 REPEATABLE READ를 사용한다.
'백엔드(Back End) > DataBase' 카테고리의 다른 글
[TIL]20230804 - Redis의 Data Type (0) | 2023.08.04 |
---|---|
[TIL]20230803 - Redis의 특징 (0) | 2023.08.04 |
[TIL]20230801 - DB 인덱스 (0) | 2023.08.02 |
[TIL]20230731 - DB 정규화 (0) | 2023.07.31 |
DB 트랜잭션(Transaction) (0) | 2023.06.19 |