[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는 shinkai
가 rollback
되더라도 여전히 데이터가 있다고 생각하고 처리할 것이라는 점이다.. 🥲
이처럼 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도
다른 트랜잭션에서 볼 수 있는 현상을 DIRTY READ
라고 합니다.
DIRETY READ
는 데이터가 보였다가 안보였다가 하는 현상을 초래하기 때문에
개발자와 사용자를 혼란스럽게 할 수 있습니다….ㅠ0ㅠ
따라서 정합성에 문제가 많은 격리수준이라고 할 수 있고,READ UNCOMMITTED
격리수준은 사용하지 않는 것이 좋습니다.
READ COMMITED
오라클 DBMS에서 사용되는 격리 수준이며, 온라인 서비스에서 가장 많이 선택되는 격리수준 입니다.READ COMMITED
는 DIRTY READ
가 발생하지 않습니다.commit
이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문입니다.!
사용자1이 데이터를 변경(seakai
)하고 commit
을 수행하기 전에
사용자2가 조회를 시도하면 seakai
가 아니라 shinkai
가 조회 됩니다.
이는 사용자2의 select
결과가 member
테이블이 아니라 undo log
에 복사된 레코드에서 가져왔기 때문입니다.
사용자1이 커밋을 하게되면, 그때 최종적으로 사용자2는 seakai
를 조회할 수 있습니다.
하지만 READ COMMITED
는 NON-REPEATABLE READ
부정합 문제를 발생할 수 있다고 언급했습니다.
어떤 상황에서 발생하는 것 일까요? 🤔
NON-REPEATABLE READ 부정합
처음에 사용자2가 트랜잭션을 시작하고 seakai
를 조회했을 때 결과가 없습니다.
그런데 사용자1이 shinkai
를 seakai
로 변경하고 commit
을 수행한 후,
사용자2가 다시 sekai
를 조회했다고 합시다.
그러면 사용자2는 seakai
를 조회할 수 있습니다.
얼핏 보면 데이터 정합성에 문제가 없는 것처럼 보이지만,
하나의 트랜잭션 내에서 똑같은 select
쿼리를 실행했을 때
항상 같은 결과를 가져와야 한다는 REPEATABLE READ
정합성에 어긋났습니다.
이를 우리는 NON-REPEATABLE READ
부정합의 문제있다고 이야기 합니다. 🥲
중요하지 않은 것처럼 느껴질수 있겠지만,
이런 문제로 데이터의 정합성이 깨지기 시작하여 애플리케이션에 버그가 발생하면
그 원인을 찾아내기 힘듭니다. ㅠ0ㅠ
참고READ COMMITTED
격리 수준에서는
트랜잭션 내에서 실행되는 select
와 트랜잭션 외부에서 실행되는 select
문의 차이가 별로 없습니다.
하지만, REPEATABLE READ
격리 수준에서는 기본적으로 select
쿼리도 트랜잭션 범위 내에서만 작동 합니다.
이말은 트랜잭션을 시작한 상태에서 온종일 동일한 쿼리를 반복해서 실행해봐도 동일한 결과만 보이게 됩니다.
(아무리 다른 트랜잭션에서 데이터를 변경하고 commit
을 한다고 해서 동일한 결과만 보이게 됩니다.^^)
REPEATABLE READ(MySQL 기본 격리 수준)
REPEATABLE READ
는 MySQL
의 InnoDB
스토리지 엔진에서 기본으로 사용되는 격리 수준 입니다.
(바이너리 로그를 가진 MySQL
서버에서는 최소 REPEATABLE READ
격리 수준 이상 사용해야 합니다.)
REPEATABLE READ
격리 수준에서는 READ COMMITTED
에서 발생했던NON-REPEATABLE READ
부정합이 발생하지 않습니다.
InnoDB
스토리지 엔진은 트랜잭션이 rollback
될 가능성에 대비하여
변경되기 전 레코드들 undo
영역에 백업해두고 실제 레코드 값을 변경합니다.
이러한 방식은 MVCC
라고 합니다.
REPEATABLE READ
는 MVCC
방식을 이용하여undo
영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있게 보장합니다.
실제로는 READ COMMITTED
도 MVCC
방식을 이용해 commit
되기 전의 데이터를 보여줍니다.
다만, REPEATABLE READ
와 READ 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
- SELECT FOR UPDATE 이후 SELECT: 갭락 때문에
- SERIALIZABLE
- 가장 단순하고, 가장 엄격한 격리 수준
- 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야만 하며, 동시에 다른 트랜잭션은 그러한 레코드를 변경하지 못함
- 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없음
참고
'0+ 스프링 > 0+스프링 DB' 카테고리의 다른 글
[스프링 DB] 트랜잭션 전파(propagation) (0) | 2023.07.29 |
---|---|
[스프링 DB] 데이터를 저장할 때 파일에 저장해도 되는데, 데이터베이스에 저장하는 이유? (0) | 2023.07.06 |
[스프링 DB] 커넥션 풀과 DataSource의 개념 (0) | 2023.07.03 |
[스프링 DB] JDBC의 개념 (0) | 2023.07.03 |