트랜잭션 고립성(Transaction isolation)
•
여러 트랜잭션들이 동시에 실행돼도 다른 트랜잭션들로부터 고립되어 실행되는 것처럼 보여야 하는 성질
→ DBMS에서는 고립성에 단계를 두어 고립되는 것처럼 보일지 아닐지 선택할 수 있다.
•
동시성과 고립성은 충돌할 수 밖에 없음.
→ 서비스 운영 시 서비스의 성격에 알맞는 격리 수준을 선택해야 한다.
고립성에 단계를 나누는 이유
•
대부분의 서비스는 읽기 관련 질의 요청 비율이 높다
→ READ 가 WRITE 보다 더 많다
ex. 게시글 조회 >> 게시글 쓰기, 상품 조회 >> 상품 주문, 등록
→ 이런 상황에서 LOCK 의 개념만 존재한다면, 대기 시간이 길어질거다
→ 그래서 고립성에 단계를 나눔
•
단계가 높아질수록 트랜잭션 간 고립 정도가 높아지고, 성능이 떨어짐
트랜잭션 고립성의 네 가지 단계
1.
READ-UNCOMMITTED (1단계)
2.
READ-COMMITTED (2단계)
3.
REPEATABLE-READ (3단계)
4.
SERIALIZABLE (4단계)
일반적인 상황
•
동시성의 문제는 발생하지 않는다.
•
세 개의 세션이 유사한 시간대에 어떤 질의를 실행한다면?
→ 고립성 단계에 따라 어떤 데이터를 읽어야 할지 달라진다.
1. READ-UNCOMMITTED
•
특정 트랜잭션에서 데이터를 변경했으나, 다른 트랜잭션에서 커밋되지 않은 데이터를 읽을 수 있음.
→ Dirty-Read
→ 커밋 전에는 히스토리 로그 / 커밋 후에는 테이블에서 데이터를 읽는다.
•
INSERT 만 진행되고 ROLLBACK 될 수도 있는 데이터를 읽을 수 있으므로 주의해야 한다.
세션 A : 명시적 트랜잭션 구문을 이용해 데이터를 변경
세션 B : 명시적인 트랜잭션 구문을 이용해 데이터를 읽음
세션 C : 단순하게 데이터를 읽음
•
세션 B, 세션 C 의 고립성 단계가 READ-UNCOMMITTED 인 경우
1.
세션 A 의 트랜잭션이 커밋되지 않은 상황
→ 히스토리 로그에 있는 데이터를 읽음.
2.
세션 A 의 트랜잭션이 커밋된 상황
→ 테이블의 데이터를 읽음
2. READ-COMMITTED
•
특정 트랜잭션에서 데이터를 변경했어도 커밋되지 않았으면 변경되기 전의 데이터를 읽을 수 있음
•
커밋 된 경우는 변경된 데이터를 읽음.
→ 커밋 전 / 후 모두 테이블에서 데이터를 읽는다.
•
Dirty Read 가 발생하지 않도록 보장
•
하나의 트랜잭션 안에서 SELECT 를 수행할 때마다 데이터가 동일하다는 보장이 없음.
→ Non-Repeatable Read 라고도 함
•
세션 B, 세션 C 의 고립성 단계가 READ-UNCOMMITTED 인 경우
1.
세션 A 의 트랜잭션이 커밋되지 않은 상황
→ 테이블의 데이터를 읽음
2.
세션 A 의 트랜잭션이 커밋된 상황
→ 테이블의 데이터를 읽음
3. REPEATABLE-READ
•
다른 트랜잭션의 커밋 여부와 상관없이 트랜잭션 시작 시점의 값을 읽음.
•
다른 트랜잭션에 영향을 받지 않고 동일한 데이터를 가져옴.
→ 한 트랜잭션 안에서 반복해서 SELECT 를 수행해도 읽어 들이는 값이 변화하지 않음을 보장
•
한 트랜잭션에서 READ 되는 데이터 수가 달라질 수 있음.
•
세션 B, 세션 C 의 고립성 단계가 REPEATABLE-READ 인 경우
•
세션 C 의 경우, 두 번의 트랜잭션이 실행된 것과 동일하다. (트랜잭션의 원자성)
1.
세션 A 의 트랜잭션이 커밋되지 않은 상황
→ 세션 B : 변경 전 히스토리 로그를 읽음
→ 세션 C : 변경 전 히스토리 로그를 읽음 (세션 A 의 변경 전 : 10000)
2.
세션 A 의 트랜잭션이 커밋 된 상황
→ 세션 B : 변경 전 히스토리 로그를 읽음
→ 세션 C : 변경 전 히스토리 로그를 읽음 (세션 A 의 변경 전 : 5000)
1.
세션 A 가 커밋되기 전 : 세션 B , 세션 C 모두 변경되기 전 값 1 : 10000 을 읽음
2.
세션 A 가 커밋된 후 : 세션 B 는 1 : 10000 , 세션 C 는 1 : 5000, 2 : 3000 을 읽음
예외적으로 인텐션 락으로 질의를 하는 경우, 같은 트랙잭션 범위 내에서도 변경된 데이터를 읽는다.
•
인텐션 락은 히스토리 로그와 상관없이, 테이블과 관련된 부분임
→ 레코드에 잠금을 걸고 테이블에서 데이터를 읽는다. (Phantom Read)
→ 인텐션 락을 이용한 읽기의 경우, 커밋 이후에 데이터를 읽으면 예상치 못한 결과 발생 가능
4. SERIALIZABLE
•
최상위 고립 단계.
•
명시적인 트랜잭션 내에서 데이터 변경 질의가 전달될 경우, 명시적인 트랜잭션 내에서 락을 가진 트랜잭션만 접근할 수 있다.
•
동시성 문제가 발생하지 않는다. (단계 중 가장 배타적임)
•
성능이 많이 떨어짐
•
세션 A 가 커밋 되기 전까지 세션 B , 세션 C 는 해당 레코드에 접근할 수 없음.
→ 락을 획득하기 위해 기다려야 함.
•
질의를 순차적으로 실행
각 격리 수준 별 발생 가능 문제
모든 DBMS 에서 네 가지 고립성 단계를 지원하는가?
•
DBMS 마다 지원 여부가 다르고, 실행 결과도 조금 다를 수 있다.
ex) 오라클 DBMS 는 공식적으로 두 단계만 지원
→ DB를 공부하기 위해서는 이론 + 각 DBMS 별 도구, 기능, 동작원리를 알아야 한다.
고립성 실습
클라이언트(세션)의 고립성 단계 변경
# 해당 세션의 transaction isolation level을 READ UNCOMMITED로 변경
SET SESSION transaction isolation level READ UNCOMMITTED;
# 해당 세션의 transaction isolation level을 READ COMMITED로 변경
SET SESSION transaction isolation level READ COMMITTED;
# 해당 세션의 transaction isolation level을 REPEATABLE READ 로 변경
SET SESSION transaction isolation level REPEATABLE READ;
# 해당 세션의 transaction isolation level을 SERIALIZABLE 로 변경
SET SESSION transaction isolation level SERIALIZABLE;
SQL
복사
클라이언트의 고립성 단계 확인
SELECT @@transaction_ISOLATION;
SQL
복사
mysql> SELECT @@transaction_ISOLATION;
+-------------------------+
| @@transaction_ISOLATION |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
SQL
복사
1. READ UNCOMMITTED
첫 번째 클라이언트
BEGIN;
UPDATE PRODUCT SET COST = 5000 WHERE ID = 1;
SQL
복사
아직 커밋되지 않은 상태
두 번째 클라이언트
BEGIN;
SELECT * FROM PRODUCT;
SQL
복사
세 번째 클라이언트
SELECT * FROM PRODUCT;
SQL
복사
아직 첫 번째 클라이언트의 트랜잭션이 커밋되지 않은 상태이지만, 두 번째, 세 번째 클라이언트는 변경된 데이터를 가져왔다.
→ Dirty Read
다음 시나리오를 위해 ROLLBACK
ROLLBACK;
SQL
복사
2. READ COMMITTED
두 번째, 세 번째 클라이언트 고립성 단계 READ COMMITTED로 변경
첫 번째 클라이언트
BEGIN;
UPDATE PRODUCT SET COST = 5000 WHERE ID = 1;
SQL
복사
두 번째 클라이언트
BEGIN;
SELECT * FROM PRODUCT;
SQL
복사
세 번째 클라이언트
SELECT * FROM PRODUCT;
SQL
복사
첫 번째 클라이언트의 트랜잭션이 커밋되지 않았을 때 두 번째, 세 번째 클라이언트는 READ COMMITTED 이므로, 변경 전 데이터를 읽어왔다.
ROLLBACK 수행
3. REPEATABLE-READ
두 번째, 세 번째 클라이언트 고립성 단계 REPEATABLE-READ로 변경
첫 번째 클라이언트
BEGIN;
UPDATE PRODUCT SET COST = 5000 WHERE ID = 1;
SQL
복사
두 번째 클라이언트
BEGIN;
SELECT * FROM PRODUCT;
SQL
복사
두 번째 클라이언트는 REPEATABLE-READ 이므로 첫 번째의 커밋 여부와 상관 없이 트랜잭션 시작 시점의 값을 읽어온다.
세 번째 클라이언트
SELECT * FROM PRODUCT;
SQL
복사
세 번째 클라이언트도 REPEATABLE-READ 이므로 트랜잭션 시작 시점의 값을 읽어온다.
첫 번째 클라이언트 커밋
두 번째 클라이언트(첫 번째 클라이언트 커밋 후)
SELECT * FROM PRODUCT;
SQL
복사
마찬가지로 첫 번째의 커밋 여부와 상관 없이 두 번째 클라이언트의 트랜잭션 시작 시점의 값을 읽어온다. → BEGIN 시점에 ID: 1 COST: 10000
세 번째 클라이언트(첫 번째 클라이언트 커밋 후)
SELECT * FROM PRODUCT;
SQL
복사
세 번째 클라이언트의 SELECT * FROM PRODUCT; 구문은 하나의 트랜잭션 이므로, 트랜잭션 시작 시점의 값인 COST: 5000 을 읽어온다.
Phantom Read 발생 예시
첫 번째 클라이언트 데이터 추가
BEGIN;
UPDATE PRODUCT SET COST = 5000 WHERE ID = 1;
INSERT INTO PRODUCT (COST) VALUES(3000);
SQL
복사
두 번째 클라이언트
여전히 트랜잭션 시작 시점의 값 COST: 10000 을 읽어온다.
세 번째 클라이언트
마찬가지로 첫 번째 클라이언트가 커밋되지 않았으므로 트랜잭션 시작 시점의 값 COST: 10000 을 읽어온다.
첫 번째 클라이언트 커밋
두 번째 클라이언트(첫 번째 클라이언트 커밋 후)
두 번째 클라이언트의 트랜잭션 시작 시점의 값 COST: 10000 을 읽어온다.
세 번째 클라이언트(첫 번째 클라이언트 커밋 후)
세 번째 클라이언트에 이번에 입력한 SELECT 문은 하나의 트랜잭션이다.
이 트랜잭션이 시작할 때의 값 COST: 5000 , COST: 3000 을 가져온다.
4. SERIALIAZABLE
두 번째, 세 번째 클라이언트 고립성 단계 SERIALIAZABLE로 변경
첫 번째 클라이언트
BEGIN;
UPDATE PRODUCT SET COST = 5000 WHERE ID = 1;
SQL
복사
두 번째 클라이언트
BEGIN;
SELECT * FROM PRODUCT;
SQL
복사
첫 번재 클라이언트의 트랜잭션이 커밋되기 전 까지 해당 레코드에 접근할 수 없다.
세 번째 클라이언트
첫 번째 클라이언트 커밋
두 번째 클라이언트(첫 번째 클라이언트 커밋 후)
두 번째 클라이언트의 트랜잭션 TIMEOUT 전에 첫 번째 클라이언트를 커밋하면, 커밋 후에 데이터를 불러온다.