0+ 스프링/0+스프링 DB

[스프링 DB] MySQL의 트랜잭션 격리 수준(Transaction Isolation Level) 파헤치기

힘들면힘을내는쿼카 2023. 8. 1. 00:06
728x90
반응형

[DB] MySQL의 트랜잭션 격리 수준(Transaction Isolation Level) 파헤치기

모든 내용은 InnoDB를 기준으로 설명합니다.^^

 

트랜잭션 격리 수준이란?

 

백엔드 개발자라면 트랜잭션 격리 수준(isolation level)이란 용어를 들어본 적이 있을 것 입니다.
그런데 트랜잭션 격리 수준 정확히 뭔가요? 🤔

 

트랜잭션 격리 수준이란
여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수있게 허용할지 말지를 결정하는 것 입니다.

 

즉, 하나의 트랜잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지 결정하는 것을 의미 합니다.

 

트랜잭션 격리 수준은 크게 4가지로 구분 됩니다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

 

참고

  • 위 4가지 격리 수준은 자동 커밋(AUTO COMMIT)이 false인 상태에서만 발생합니다.
  • DIRTY READ라고 하는 READ UNCOMMITTED는 일반적으로 사용하지 않습니다.
  • SERIALIZABLE는 동시성이 아주 중요한 데이터베이스에서는 거의 사용되지 않습니다.

 

3가지 부정합

데이터베이스의 격리 수준을 언급하면 항상 함께 따라오는 녀석이 부정합 문제점 입니다.
3가지 부정합의 문제점은 격리 수준에 따라 발생할 수도 있고 발생하지 않을 수도 있습니다.

아래는 격리수준에 따른 부정합 문제점 발생 여부를 나타내는 표 입니다.

참고
SQL-92 or SQL-99 표준에 따르면,
REPEATABLE READ 격리 수준에서는 PHANTOM READ가 발생할 수 있지만,
InnoDB에서는 독특한 특성 때문에 발생하지않습니다.

 

READ UNCOMMITTED(권장 X)

이름에서 알 수 있듯이 커밋 또는 롤백 여부에 관계 없이 다른 트랜잭션에서 데이터를 읽을 수 있습니다.

 

사용자1이 커밋하기 전인데 사용자2는 shinkai를 조회할 수 있다.
별 문제 없어 보인다.
그런데 사용자1이 처리 도중에 알 수 없는 문제가 발생하여 insert 쿼리를 롤백한다고 해보자…
사용자2는 shinkairollback되더라도 여전히 데이터가 있다고 생각하고 처리할 것이라는 점이다.. 🥲

 

이처럼 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도
다른 트랜잭션에서 볼 수 있는 현상DIRTY READ라고 합니다.

 

DIRETY READ는 데이터가 보였다가 안보였다가 하는 현상을 초래하기 때문에
개발자와 사용자를 혼란스럽게 할 수 있습니다….ㅠ0ㅠ

 

따라서 정합성에 문제가 많은 격리수준이라고 할 수 있고,
READ UNCOMMITTED 격리수준은 사용하지 않는 것이 좋습니다.

 

READ COMMITED

오라클 DBMS에서 사용되는 격리 수준이며, 온라인 서비스에서 가장 많이 선택되는 격리수준 입니다.
READ COMMITEDDIRTY READ가 발생하지 않습니다.
commit이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문입니다.!

사용자1이 데이터를 변경(seakai)하고 commit을 수행하기 전에
사용자2가 조회를 시도하면 seakai가 아니라 shinkai가 조회 됩니다.
이는 사용자2의 select 결과가 member 테이블이 아니라 undo log에 복사된 레코드에서 가져왔기 때문입니다.

 

사용자1이 커밋을 하게되면, 그때 최종적으로 사용자2는 seakai를 조회할 수 있습니다.

 

하지만 READ COMMITEDNON-REPEATABLE READ 부정합 문제를 발생할 수 있다고 언급했습니다.
어떤 상황에서 발생하는 것 일까요? 🤔

 

NON-REPEATABLE READ 부정합

처음에 사용자2가 트랜잭션을 시작하고 seakai를 조회했을 때 결과가 없습니다.
그런데 사용자1이 shinkaiseakai로 변경하고 commit을 수행한 후,
사용자2가 다시 sekai를 조회했다고 합시다.
그러면 사용자2는 seakai를 조회할 수 있습니다.

 

얼핏 보면 데이터 정합성에 문제가 없는 것처럼 보이지만,
하나의 트랜잭션 내에서 똑같은 select 쿼리를 실행했을 때
항상 같은 결과를 가져와야 한다REPEATABLE READ 정합성에 어긋났습니다.

 

이를 우리는 NON-REPEATABLE READ 부정합의 문제있다고 이야기 합니다. 🥲

 

중요하지 않은 것처럼 느껴질수 있겠지만,
이런 문제로 데이터의 정합성이 깨지기 시작하여 애플리케이션에 버그가 발생하면
그 원인을 찾아내기 힘듭니다. ㅠ0ㅠ

 

참고
READ COMMITTED 격리 수준에서는
트랜잭션 내에서 실행되는 select와 트랜잭션 외부에서 실행되는 select 문의 차이가 별로 없습니다.

하지만, REPEATABLE READ 격리 수준에서는 기본적으로 select 쿼리도 트랜잭션 범위 내에서만 작동 합니다.
이말은 트랜잭션을 시작한 상태에서 온종일 동일한 쿼리를 반복해서 실행해봐도 동일한 결과만 보이게 됩니다.
(아무리 다른 트랜잭션에서 데이터를 변경하고 commit을 한다고 해서 동일한 결과만 보이게 됩니다.^^)

 

REPEATABLE READ(MySQL 기본 격리 수준)

REPEATABLE READMySQLInnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준 입니다.
(바이너리 로그를 가진 MySQL 서버에서는 최소 REPEATABLE READ 격리 수준 이상 사용해야 합니다.)

 

REPEATABLE READ격리 수준에서는 READ COMMITTED에서 발생했던
NON-REPEATABLE READ 부정합이 발생하지 않습니다.

 

InnoDB 스토리지 엔진은 트랜잭션이 rollback 될 가능성에 대비하여
변경되기 전 레코드들 undo 영역에 백업해두고 실제 레코드 값을 변경합니다.
이러한 방식은 MVCC라고 합니다.

 

REPEATABLE READMVCC 방식을 이용하여
undo 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있게 보장합니다.
실제로는 READ COMMITTEDMVCC 방식을 이용해 commit 되기 전의 데이터를 보여줍니다.

 

다만, REPEATABLE READREAD COMMITTED 차이는
undo 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가야 하느냐에 있습니다.

 

undo 영역

모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호(순차적으로 증가)를 가지며,
undo 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션의 번호(TRX-ID)가 포함되어 있습니다.

 

REPEATABLE READ 격리 수준이 작동하는 방식

 

REPEATABLE READ 격리 수준에서는 MVCC를 보장하기 위해
실행 중인 트랜잭션 가운데 가장 오래된 트랜잭션 번호(TRX-ID: 10)보다 트랜잭션 번호가 앞선 undo 영역(TRX-ID: 6)의 데이터를 삭제할 수가 없습니다.

 

undo 영역에 백업된 데이터가 1개만 있는 것으로 표현했지만,
실제로는 더 많은 백업이 존재할 수 있습니다.

 

만약 트랜잭션을 시작하고 장시간 트랜잭션을 종료하지 않으면
undo 영역에 백업된 데이터가 무한정 커질 수도 있습니다. 🫨

 

PHANTOM READ 부정합

REPEATABLE READ 격리 수준에서도 PHANTOM READ 부정합이 발생할 수 있습니다.

 

그런데, 이상한점이 있습니다. 🤔
REPEATABLE READ에서 우리는 하나의 트랜잭션 내에서 select 쿼리의 결과는 똑같아야 한다는 것을 배웠습니다.

 

select … for update 쿼리는 select 하는 레코드에 쓰기 잠금을 걸어야하는데,
undo 레코드에는 잠금을 걸 수 없습니다. 🥲

 

따라서 select ... for update 또는 select … lock in share mode 로 조회되는 레코드는
undo 영역의 백업 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져오는 것 입니다.

 

참고
InnoDB 스토리지 엔진에서는 갭 락넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도
PHANTOM READ 부정합 문제가 발생하지 않습니다.
(SELECT 이후 SELECT FOR UPDATE를 사용할 경우 PHANTOM READ가 발생할 수 있습니다.)

 

정리

 

SERIALIZABLE(가장 엄격)

SERIALIZABLE가장 단순한 격리 수준과 동시에 가장 엄격한 격리 수준 입니다.
그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어집니다.

 

InnoDB 테이블에 기본적으로 순수한 select 작업은 레코드 잠금도 설정하지 않고 실행합니다.

 

하지만, SERIALIZABLE로 설정되면 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야만 하며,
동시에 다른 트랜잭션은 그러한 레코드를 변경하지 못하게 됩니다.

 

한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없다는 의미 입니다.

 

SERIALIZABLE 격리 수준에서는 PHANTOM READ 부정합 문제도 발생하지 않습니다.
하지만, InnoDB 스토리지 엔진에서는 갭 락넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도
PHANTOM READ 부정합 문제가 발생하지 않기 때문에 굳이 SERIALIZABLE를 사용할 필요는 없을 것 같습니다.^^

 

정리

  • READ UNCOMMITTED
    • 커밋, 롤백 여부에 관계 없이 다른 트랜잭션이 데이터를 읽을 수 있음
    • DIRTY READ 부정합 발생
    • 권장하지 않음
  • READ COMMITTED
    • 커밋이 완료된 데이터만 다른 트랜잭션에서 데이터를 읽을 수 있음
    • NON-REPEATABLE READ 부정합 문제를 발생 가능
      • 하나의 트랜잭션 내에서 같은 select문을 사용했음에도 다른 결과가 나오는 경우
  • REPEATABLE READ(MySQL 기본)
    • READ COMMITTED에서 발생하는 NON-REPEATABLE READ 부정합 문제 해결
      • 실행중인 트랜잭션 번호(TRX: 10) 이전의 트랜잭션 번호(TRX: 6)의 undo 영역 백업 데이터를 조회
    • 장시간 트랜잭션을 종료하지 않으면 undo에 백업된 데이터의 용량이 무한정 커질 수 있음
    • PHANTOM READ 부정합 문제가 발생
      • select ... for update를 사용할 경우 undo 레코드에는 잠금을 걸 수 없기 때문
      • InnoDB 스토리지 엔진에서는 갭 락넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 PHANTOM READ 부정합 문제가 발생하지 않음
        • SELECT FOR UPDATE 이후 SELECT: 갭락 때문에 PHANTOM READ X
        • SELECT FOR UPDATE 이후 SELECT FOR UPDATE: 갭락 때문에 PHANTOM READ X
        • SELECT 이후 SELECT: MVCC 때문에 PHANTOM READ X
        • SELECT 이후 SELECT FOR UPDATE: PHANTOM READ O
  • SERIALIZABLE
    • 가장 단순하고, 가장 엄격한 격리 수준
    • 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야만 하며, 동시에 다른 트랜잭션은 그러한 레코드를 변경하지 못함
    • 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없음

 

참고

 

 

 

728x90
반응형