/* 트랜잭션 격리수준*/
트랜잭션의 격리수준 (Isolation Level) 이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 여부를 결정하는 것이다. 트랜잭션의 격리 수준은 격리 수준이 높은 순서대로
- Serializable
- Repeatable Read
- Read Commited
- Read Uncomitted
로 나뉘어 진다.
/* Serializable */
Serializable 격리 수준은, 이름 그대로 트랜잭션들을 순차적으로 진행시킨다. 여러 트랜잭션이 동일한 레코드에 동시 접근할 수 없게 막으므로, 어떠한 데이터 부정합 문제도 발생하지 않는다. 하지만, 트랜잭션이 순차적으로 처리되어야 하므로 동시 처리 성능이 매우 떨어진다.
Serializable 격리 수준에서는 SELECT 작업에서도 대상 레코드에 넥스트 키 락을 읽기 잠금 (공유락, Shared Lock)으로 건다. 따라서 다른 트랜잭션에서 그 레코드를 추가/수정/삭제할 수 없다.
ex)
<T1>
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 1;
# 넥스트 키 락과 읽기 잠금(Shared Lock)이 설정됩니다.
<T2>
UPDATE accounts SET balance = balance + 500 WHERE account_id = 1;
# T2는 T1이 COMMIT 또는 ROLLBACK을 완료하기 전까지 대기해야 한다.
성능이 떨어진다는 단점이 있다.
/* Repeatable Read */
일반적인 RDBMS는 변경 전의 레코드를 백업해준다. 그러면 변경 전/후 데이터가 모두 존재하므로, 동일한 레코드에 대하여 여러 버전이 존재한다고 하여 이를 MVCC (Multi-Version Concurrency Control, 다중 버전 동시성 제어)라고 부른다. 따라서 트랜잭션이 롤백된 경우 데이터를 복원할 수 있고 트랜잭션 간에 접근 할 수 있는 데이터를 제어할 수 있다. 각각의 트랜잭션은 고유한 번호가 존재하며, 백업 레코드에는 어느 트랜잭션에 의해 백업되었는지 트랜잭션 번호를 함께 저장한다.
Repeatable Read는 MVCC를 이용해 한 트랜잭션 내에서 동일한 결과를 보장하지만, 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.
Ex)
사용자 B 에서는 id >= 50 인 레코드를 조회하면 1건이 조회된다. (아직 트랜잭션은 종료되지 않았다.)
이때 다른 사용자 A (트랜잭션 12) 에서 id = 50 인 레코드를 업데이트 한다. 이러면 MVCC를 통해 기존 데이터는 변경되지만 백업된 데이터가 언두 로그에 남게 된다.
여기서 사용자 B (트랜잭션 B) 가 id >= 50 인 레코드를 조회한다고 하면 어떻게 될까?
사용자 B의 트랜잭션 (10)은 사용자 A의 트랜잭션 (12)이 시작하기 전에 이미 시작 되었다. 이때 Repeatable Read는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고하여 데이터를 조회한다.
위처럼 Repeatable Read는 새로운 레코드의 추가는 막지 않는다. 따라서 SELECT로 조회한 경우 트랜잭션이 끝나기 전에 다른 트랜잭션에 의해 추가된 레코드가 발견될 수 있는데 이를 유령 읽기 (Phantom Read)라고 한다. 하지만 일반적인 조회에서 Phantom Read는 발생하지 않는다. 왜냐하면 자신보다 나중에 실행된 트랜잭션이 추가한 레코드는 무시하면 되기 때문이다. 아래 그림 처럼 말이다.
Repeatable Read에서 Phantom Read를 잠금이 사용되는 경우에 발생할 수 있다. MySQL은 다른 RDBMS와 다르게 특수한 갭 락이 존재하기 때문에, 동작이 다른 부분이 있으므로 일반적인 RDBMS 부터 보자.
사용자 B가 먼저 데이터를 조회하는데, SELECT FOR UPDATE 를 이용해 쓰기 잠금을 걸었다. SELECT ... FOR UPDATE 구문은 베타락 (비관적 잠금, 쓰기 잠금)을 거는 것이다. 읽기 잠금은 SELECT FOR SHARE이다. 락은 트랜잭션이 커밋, 롤백 될 때 해제된다.
그리고 사용자 A가 새로운 데이터를 INSERT하는 상황이라고 생각하자. 일반적인 DBMS에서는 갭락이 존재하지 않으므로 id = 50인 레코드만 잠금이 걸린 상태이고 사용자 A의 요청은 즉시 실행된다.
이 때 사용자 B가 동일한 쓰기 잠금 쿼리로 다시 한번 데이터를 조회하면, 이번에는 2건의 데이터가 조회된다. 동일한 트랜잭션 내에서도 새로운 레코드가 추가되는 경우에 조회 결과가 달라지는데, 이를 Phantom Read라고 한다.
아 경우에는 MVCC를 통해 해결이 안된다. SELECT FOR UPDATE 구문을 통해 발생한 잠금 있는 읽기는 데이터 조회가 언두 로그가 아닌 테이블에서 수행된다. 락이 걸린 트랜잭션 내에서의 읽기는 테이블에 변경이 일어나지 않도록 테이블에 잠금을 걸고 테이블에서 데이터를 조회한다. 하지만 새로운 레코드의 삽입 또는 기존 레코드의 값 변경하는 경우에는 잠금을 설정하지 않는다.
하지만 MySQL에서는 갭 락이 존재하기 때문에 위의 상황에서 문제가 발생하지 않는다.
사용자 B가 SELECT FOR UPDATE 로 데이터를 조회한 경우에 MySQL은 id 가 50인 레코드에는 레코드 락, id가 50보다 큰 범위에서는 갭 락으로 넥스트 키 락을 건다. 따라서 사용자 A가 id가 51인 member를 INSERT 시도한다면, B의 트랜잭션이 종료 (커밋 or 롤백)될 때까지 기다리다가 실행한다. (대기를 오래하면 락 타임아웃이 발생한다.)
따라서 일반적으로 MySQL의 Repeatable Read에서는 Phantom Read가 발생하지 않는다. MySQL에서 Phantom Read가 발생하는 케이스는 다음과 같다.
처음에 사용자 B가 트랜잭션을 시작하고 잠금없는 SELECT 문으로 데이터를 조회할 때 발생한다.
하지만 이러한 케이스는 거의 존재하지 않는다.
마지막으로 트랜잭션 내에서 실행되는 SELECT와 트랜잭션 없이 실행되는 SELECT의 차이를 살펴보도록 하자. Repeatable Read에서는 트랜잭션 번호를 바탕으로 실제 테이블 데이터와 언두 로그의 데이터를 비교하여 어떤 데이터를 조회할지 판단한다. 즉, 트랜잭션 안에서 실행되는 SELECT라면 항상 일관된 데이터를 조회하게 된다. 하지만 트랜잭션 없이 실행된다면, 데이터의 정합성이 깨지는 상황이 발생할 수 있다.
/* Read Commited */
Read Commited는 커밋된 데이터만 조회할 수 있다. Read Commited는 Phantom Read에 더해 Non-Repeatable Read (반복 읽기 불가능) 문제까지 발생한다.
Ex)
사용자 A가 트랜잭션을 시작하여 어떤 데이터를 변경하였고 아직 커밋은 하지 않았다. 그러면 테이블은 먼저 갱신되고, 언두 로그로 변경 전의 데이터가 백업된다.
이때 사용자 B가 데이터를 조회한다고 하면, Read Commited 에서는 커밋된 데이터만 조회할 수 있으므로 언두 로그에서 변경 전의 데이터를 찾아서 반환하게 된다.
하지만 Read Commited는 Non-Repeatble Read (반복 읽기 불가능) 문제가 발생할 수 있다.
예를 들어 사용자 B가 트랜잭션을 시작하고 name = "MinKyu"인 레코드를 조회했다고 하면 해당 레코드는 아직 존재하지 않으므로 아무 것도 반환 되지 않는다.
그러다가 사용자 A가 UPDATE 문을 수행하고 커밋을 완료하여 해당 조건을 만족하는 레코드가 생겼다고 하자. Read Commited 에서는 커밋된 데이터는 조회할 수 있으므로 결과가 나온다.
Read Commited에서 반복 읽기를 수행하면 다른 트랜잭션의 커밋 여부에 따라 조회 결과가 달라질 수 있다. 따라서 이런 데이터 부정합 문제를 Non-Repeatable Read라고 한다.
Read Commited는 커밋된 데이터만 읽을 수 있기 때문에 트랜잭션 내에서 실행되는 SELECT와 트랜잭션 밖에서 실행되는 SELECT의 차이가 별로 없다.
/* Read Uncommited */
Read Uncommited는 커밋하지 않은 데이터 조차도 접근할 수 있다. Read Uncommited에서는 다른 트랜잭션의 작업이 커밋 또는 롤백되지 않아도 즉시 보인다.
이렇듯 어떤 트랜잭션의 작업이 완료되지 않았는데, 다른 트랜잭션에서 볼 수 있는 문제는 Dirty Read라고 한다. Dirty Read는 데이터가 조회되었다가 사라지는 현상을 초래하므로 시스템에 혼란을 주게 된다. 그래서 최소 Read Commited 이상의 격리 수준을 사용하는 것이 좋다.
'데이터베이스' 카테고리의 다른 글
[MySQL] 인덱스 (0) | 2024.06.03 |
---|---|
SQL 문법 정리 #1 (1) | 2023.11.13 |
MySQL 리눅스와 터미널연결 및 MySQL 설치 (0) | 2023.10.27 |
DB (MySQL) 공부시작 (1) | 2023.10.26 |