트랜잭션 격리 수준(Isolation level)

leehyunjon·2022년 7월 19일
1

DataBase

목록 보기
1/1

트랜잭션 격리 수준

트랜잭션 격리 수준이란, 동시에 여러개의 트랜잭션이 수행될 때 각 트랜잭션이 얼만큼의 고립성을 가지는지 나타내는 것.

즉, 특정 트랜잭션이 다른 트랜잭션에 변경된 데이터를 보여줄것인지에 대한 여부를 나타내는 것.

격리 수준은 4가지로 나뉜다.

  • Read UnCommited
  • Read Commited
  • Repetable Read
  • Serializable

Read UnCommited

특정 트랜잭션에서 변경된 데이터를 트랜잭션이 commit(), Rollback 여부와 상관없이 다른 트랜잭션에게 보여지는 격리 수준.

참조 : https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

  1. A 트랜잭션에서 데이터를 수정한다.
  2. B 트랜잭션에서 A 트랜잭션이 commit()되기 전에 조회를 한다.(Dirty Read)
  3. A 트랜잭션이 Rollback됨.(수정된 데이터가 사라짐)
  4. B 트랜잭션은 수정된 데이터가 존재하는 것으로 유지되어 로직 수행.(데이터 정합성 문제)

다른 트랜잭션이 commit()되기 전의 데이터도 조회가 가능하기 때문에 Dirty Read, Phantom Read, Non-Repetable Read가 발생한다.

Dirty Read

Dirty Read란 다른 트랜잭션의 중간 처리 작업의 결과를 볼 수 있는 현상.

즉, 트랜잭션이 커밋되기 전에 수정된 데이터를 다른 트랜잭션이 볼 수 있는 현상.

격리 수준이 Read UnCommited 이하일 때 나타나는 현상.

Non-Repetable Read

Non-Repetable Read란 하나의 트랜잭션에서 두번의 동일한 조회를 하였을 때, 서로 다른 결과가 조회되는 현상.

격리 수준이 Read Commited 이하일 때 나타나는 현상.

특정 데이터에 대한 수정이 발생하여 나타나는 현상.

Phantom Read

Phantom Read란 하나의 트랜잭션에서 두번의 동일한 조회를 하였을 때, 다른 트랜잭션의 Insert로 인해 없던 데이터가 조회되는 현상.

격리 수준이 Repetable Read 이하일 때 나타나는 현상.


Read Commited

특정 트랜잭션에서 변경된 값은 commit() 후 다른 트랜잭션에게 조회된다.

Oracle과 같은 대부분의 RDB에서 기본적으로 사용되는 격리 수준이다.

commit()후 수정된 데이터를 조회할 수 있기 때문에 Dirty Read는 발생하지 않는다.

하지만 commit()되기 전 조회시 Undo영역에 있던 데이터를 읽어오고, commit()후 조회시 수정된 데이터를 읽어오기 때문에 동일한 조회이지만 서로 다른 데이터를 조회해 오기 때문에 Non-Repetable Read가 발생하고 다른 트랜잭션의 Insert작업으로 인해 Phantom Read가 발생한다.

Read Commited의 Non-Repetable Read

참조 : https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

  1. A 트랜잭션에서 Busan을 Jeju로 변경한다.
  2. A 트랜잭션이 commit()되지 않았기 때문에 B 트랜잭션에서는 Undo영역에 있는 수정 전 데이터 Busan을 조회한다.
  3. A 트랜잭션이 commit()
  4. B 트랜잭션이 Busan을 조회하게 되면 A 트랜잭션이 commit()되었기 때문에 조회되는 데이터는 Jeju이다. (Repetable Read 부정합)

Repeatable Read

트랜잭션마다 ID를 부여하고 트랜잭션 ID보다 작은 트랜잭션을 가지고 commit()된 데이터만 조회된다.

즉, 트랜잭션이 시작되기 전에 커밋된 데이터만 조회한다.

MySQL이 사용한다.

변경되는 데이터는 Undo영역에 트랜잭션 ID와 함께 백업하고 실제 레코드 값을 변경한다.

  • 백업된 데이터가 불필요하다가 판단하는 시점에 주기적으로 삭제한다.
  • 이러한 변경 방식을 MVCC(Multi Version Concurrency Control)이라고 부른다.

Repeatable Read

참조 : https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

  1. B트랜잭션을 실행하여 Busan을 조회한다.
  2. A트랜잭션을 실행하여 Busan을 Jeju로 변경하고 commit().
    • 이 때 Undo영역에 Busan을 저장한다.
  3. B트랜잭션이 Busan을 조회하였을때 레코드 영역의 Jeju가 아닌 Undo영역의 Busan이 조회된다.

Repetable Read 격리 수준에서는 Non-Repetable Read 부정합이 발생하지 않는다.

하지만 Update 부정합Phantom Read가 발생한다.

Update 부정합

A트랜잭션이 수행중일 때 B트랜잭션이 수행하여 데이터를 수정했다면 그 이후 A트랜잭션이 수정된 데이터를 수정할 때 수정이 되지 않는 현상.

START TRANSACTION; -- transaction id : 1
SELECT * FROM Member WHERE name='junyoung';

    START TRANSACTION; -- transaction id : 2
    SELECT * FROM Member WHERE name = 'junyoung';
    UPDATE Member SET name = 'joont' WHERE name = 'junyoung';
    COMMIT;

UPDATE Member SET name = 'zion.t' WHERE name = 'junyoung'; -- 0 row(s) affected
COMMIT;

해당 결과는 name = 'joont'이다.

이유는 Repeatable Read의 특징에서 데이터가 수정될 때 이전 데이터는 Undo영역에 저장된다고 했다.

2번 트랜잭션이 이름을 junyoung에서 joont로 변경 후, 1번 트랜잭션이 junyoung을 수정하기 위해 조회할 때 junyoung은 레코드 영역이 아닌 Undo영역에 존재하게 된다.

Repeatable Read에서 Update시 쓰기 잠금이 발생하는데, Undo영역의 데이터는 쓰기 잠금을 걸 수가 없다.

그렇기 때문에 Update를 하기 위한 조건을 레코드 영역에서 찾게되는데 당연히 name = 'junyoung'이라는 조건의 데이터는 레코드 영역에 존재하지 않는다. 고로 0개의 데이터를 출력하게 되고 아무 변경도 일어나지 않게 된다.
그리고 결과는 2번 트랜잭션에서 수정된 joont가 되게 된다.

Phantom Read

다른 트랜잭션에서 Insert시 이전 조회에서 나오지 않았던 새로운 데이터가 이후 조회에서 조회되는 현상.

참조 : https://zzang9ha.tistory.com/381#2-3-repeatable-read

Repeatable Read에서는 해당 트랜잭션 이전에 커밋된 데이터만 읽어오기 때문에 Phantom Read가 어떻게 발생할 수 있지? 라는 생각을 했었다.

이를 이해하기 위해서는 Update쿼리시 발생하는 쓰기 잠금에 대해서 이해를 해야한다.
https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/

위 그림에서 첫번째 SELECT FOR UPDATE 쿼리에서는 1개의 결과를 받았지만, 두번째 SELECT FOR UPDATE 쿼리에서는 다른 트랜잭션에 의한 Insert로 2개의 결과를 받게 된다.

SELECT FOR UPDATE 쿼리는 Select한 row에 쓰기 잠금을 걸게 되는데, 쓰기 잠금은 Undo영역의 데이터에는 적용되지 않는다. 그렇기 때문에 실제 레코드 영역에 있는 데이터를 가져오기 때문에 두번째 SELECT FOR UPDATE시 row가 추가된 2개의 결과를 받게 되는 것이다.

하나의 예시로 https://jyeonth.tistory.com/32 님께서 작성하신 글에서 MySQL에서의 결과를 보자면

빨간색은 Tx1, 초록색을 Tx2 트랜잭션이, count=0 이라고 할때

  1. Tx1 실행
  2. Tx2 실행
  3. Tx1 에서 id=2인 row의 count+1로 UPDATE
  4. Tx2 에서 id=2인 row의 count+1로 UPDATE
    • 하지만 Tx1에서 id=2인 row에 대해 쓰기 잠금을 했기 때문에 Tx2의 UPDATE쿼리는 락이 걸리게 된다.
  5. Tx1 commit
    • Tx1가 commit됨으로써 id=2인 row에 대한 락이 풀리면서 4번에서의 Tx2 UPDATE쿼리가 실행된다.
  6. Tx1 에서 id=2 row를 SELECT시 count = 1로 +1 수정된것으로 확인된다.
    • 3번에서의 UPDATE 결과
  7. Tx2 에서 id=2 row를 SELECT시 count = 2로 +2 수정된것으로 확인된다.

눈여겨 봐야할 것은 7번 Tx2에서 SELECT의 결과이다.

MySQL은 Repeatable Read라 Tx2가 실행되기 전에는 분명 count=0이었을텐데, Tx2의 UPDATE로 count=1이 아닌 count=2로 수정이 되어있다.

이유는 위에서 설명했듯이 UPDATE의 쓰기 잠금의 영향을 받은 것이다.

Tx2에서 UPDATE시 id=2의 count=0이다. 하지만 이는 Tx1의 UPDATE로 인해 Undo영역에 있는 count=0이고 레코드 영역의 count는 1이다.

그렇기 때문에 Tx2에서 UPDATE는 id=2의 Undo영역의 count=0인 row가 아닌 레코드 영역의 count=1를 가져와 +1을 업데이트했기 때문에 결과는 count=2가 되는 것이 맞다.


Serializable

기본적으로 가장 순수한 Select 작업에는 잠금을 걸지않고 동작하지만, Serializable 격리 수준에서는 읽기 작업에서도 공유 잠금을 설정하여 동시에 다른 트랜잭션이 조회중인 레코드를 변경할 수 없게 된다.

가장 단순하고 가장 엄격한 격리기준.

일관성이 가장 높고 동시성이 가장 낮은 격리기준.


Reference

https://joont92.github.io/db/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-isolation-level/

https://zzang9ha.tistory.com/381#2-3-repeatable-read

https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/

https://jyeonth.tistory.com/32

https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

profile
내 꿈은 좋은 개발자

0개의 댓글