///////
Search
4️⃣

트랜잭션 동시성과 고립성

트랜잭션 고립성(Transaction isolation)

여러 트랜잭션들이 동시에 실행돼도 다른 트랜잭션들로부터 고립되어 실행되는 것처럼 보여야 하는 성질
→ DBMS에서는 고립성에 단계를 두어 고립되는 것처럼 보일지 아닐지 선택할 수 있다.
동시성과 고립성은 충돌할 수 밖에 없음.
→ 서비스 운영 시 서비스의 성격에 알맞는 격리 수준을 선택해야 한다.

고립성에 단계를 나누는 이유

대부분의 서비스는 읽기 관련 질의 요청 비율이 높다
READWRITE 보다 더 많다
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 전에 첫 번째 클라이언트를 커밋하면, 커밋 후에 데이터를 불러온다.