트랜잭션 격리 레벨(isolation level)이란 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 것이다.
즉 다르게 말하면 ⭐️여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
트랜잭션의 격리 수준에는 아래 4가지가 있다.
아래로 갈수록 트랜잭션 간의 고립 정도가 높아지고 성능이 떨어진다.
일반적으로 READ COMMITED나 REPEATABLE READ 중 하나를 사용한다고 한다.
이 글에서는 위의 네가지를 하나하나 살펴보며 각각의 격리 레벨이 어느 정도로 트랜잭션 간의 고립을 나타내는지 알아보려고 한다.
우선 격리 레벨이 가장 낮은 READ UNCOMMITED
이다.
READ UNCOMMITTED 격리 레벨에서는 어떤 트랜잭션의 변경 내용이 COMMIT, ROLLBACK 되었는지와 상관없이 다른 트랜잭션에게 보여진다. (즉, 커밋 전인데도 다른 트랜잭션이 변경 사항을 볼 수가 있다.)
이 레벨에서는 아래와 같이 동작했을 때 문제가 생길 수 있다.
🚨Ex)
1. 1번 트랜잭션이 10번 사원의 나이를 27->28로 변경했다.
2. 아직 1번 트랜잭션이 커밋되지 않고 다른 쿼리문을 실행한다.
3. 2번 트랜잭션이 시작되었고, 10번 회원의 나이를 조회하였다.
4. 28살이 조회되었다. (이를 더티 리드(Dirty Read)
라고 한다.
5. 1번 트랜잭션에서 문제가 발생해 ROLLBACK 되었다.
6. 2번 트랜잭션은 10번 사원이 28살이라고 생각하고 나머지 로직을 수행한다.
이런식으로 데이터 부정합 문제가 자주 발생할 수 있으므로 RDMS 표준에서 격리 수준으로 인정하지도 않는다고 한다.
READ COMMITTED
는 READ UNCOMMITTED
다음으로 격리 수준이 낮은 레벨이다. 이 레벨에서는 어떤 트랜잭션의 변경 내용이 commit 되어야만 다른 트랜잭션에서 조회할 수 있다.
오라클 DBMS에서 기본으로 사용하고 있고, 온라인 서비스에서 가장 많이 선택되는 격리 수준이라고 한다.
📌 Ex) READ COMMITTED 동작 예시
사용자 A, B가 있다고 하자.
READ COMMITTED
에서는 우선 테이블을 변경한 후, undo log
에 변경 전의 데이터가 백업되게 된다.READ COMMITTED
에서는 커밋된 변경사항만 조회할 수 있으므로 undo log
에서 변경 전의 데이터를 찾아 반환하게 된다.이 레벨에서는 Phantom Read, Non-Repeateable Read(반복 읽기 불가능), 문제가 발생할 수 있다.
🚨 Ex) 반복 읽기 불가능 발생 예시
1. 원래 테이블에서 10번 직원의 이름이 "clean"이었다.
2. 사용자 A가 트랜잭션을 시작하고 10번 직원의 이름을 "sejeong"으로 변경하였다. 10번 직원의 기존 이름 "clean"은 언두로그에 저장된다.
3. 사용자 B가 새로운 트랜잭션을 시작하고 10번 직원의 이름을 조회하였다. 아직 사용자 A의 트랜잭션이 커밋되지 않았으므로, 언두 로그에서 "clean"이라는 값을 읽어온다.
4. 사용자 A의 트랜잭션이 커밋되었다. (이제 다른 트랜잭션에서도 "sejeong"이라는 변경된 값을 볼 수 있다.)
5. 사용자 B의 트랜잭션에서 다시 10번 직원의 이름을 조회하였고, "sejeong"이라는 값을 읽었다. 같은 트랜잭션에서 같은 데이터를 첫번째 조회했을 때의 값은 "clean"인데, 두번째 조회했을 때의 값은 "sejeong"이 되었다. -> 일관성 X. 데이터 부정합
READ COMMITTED
에서 반복 읽기를 수행하면 다른 트랜잭션의 커밋 여부에 따라 조회 결과가 달라질 수 있다.
REPEATABLE READ
격리 수준은 간단히 말해 트랜잭션이 시작하기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리 수준이다.
트랜잭션의 번호를 비교하여 커밋된 트랜잭션 중에서 자신보다 트랜잭션 번호가 작은 트랜잭션이 변경한 내용만 조회할 수 있다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고하여 데이터를 조회한다.
(모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호(순차적을 증가)를 가지고 있으며, 언두 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있기 때문에 가능하다.)
Mysql에서 기본 격리 수준으로 사용하고 있고, 이 격리 수준에서는 앞에서 보았던 반복 읽기 불가 문제가 발생하지 않는다.
하지만 REPEATABLE READ
레벨에서는 다른 트랜잭션에서 데이터를 추가/삭제하고, 내가 쓰기잠금을 걸고 데이터를 읽어올 때 Phantom Read라는 데이터 부정합이 발생할 수 있다.
Phantom Read
란 다른 트랜잭션에서 수행한 작업에 의해 어떤 레코드가 보였다 안보였다하는 상황을 의미한다.
🚨 Ex) Phantom Read가 발생하는 상황
SELECT .. WHERE emp_no >= 50000 FOR UPDATE
쿼리 실행. 결과는 'JuBal' 하나 리턴INSERT INTO employees VALUES (50001, 'Lara');
쿼리를 실행SELECT .. WHERE emp_no >= 50000 FOR UPDATE
쿼리 실행. 결과가 ('JuBal', 'Lara') 2개 리턴 -> 부정합 발생MVCC 덕에 일반적인 조회(아무런 락을 걸지 않는 그냥 SELECT)에서는 팬텀 리드가 발생하지 않는다. 테이블에 나보다 나중에 시작한 트랜잭션이 변경한 내용이 있다면 무시하고 언두에서 읽어오기 때문이다.
이런 팬텀 리드가 발생하는 경우는 바로 잠금을 걸 때 이다. SELECT ... FOR UPDATE
같은 구문은 잠금을 거는 읽기는 언두 영역이 아닌 테이블에서 값을 읽어오기 때문이다. (언두 영역에는 락을 걸 수가 없음)
참고: Mysql의 갭락
mysql에서는 갭락 때문에 팬텀리드가 거의 발생하지 않는다.
위의 예시 상황에서, emp_id >= 50000인 레코드를SELECT .. FOR UPDATE
로 조회하면, emp_id=50000에는 레코드락을, 50001부터는 갭락으로 넥스트 키락을 걸어둔다. 그리고 다른 트랜잭션이 emp_id가 50001 이상인 레코드를 추가하려고 하면, 쓰기 잠금을 걸었던 트랜잭션이 커밋 또는 롤백 될 때까지 기다리게 한다. 기다리다가 대기 시간이 너무 길면 타임아웃이 나기도 한다.
따라서 mysql은 위의 예시 같은 케이스에서 팬텀리드가 발생하지 않지만,trx10이 SELECT -> trx12가 INSERT -> trx12가 커밋 -> trx10이 SELECT .. FOR UPDATE
이런 순서로 실행된다면 팬텀리드가 발생한다.
SERIALIZABLE
은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 실행하며, 한 트랜잭션이 같은 레코드에 동시 접속할 수 없다.
그렇기 때문에 어떤 데이터 부정합 문제도 발생하지 않는다. 하지만 트랜잭션이 하나하나 순차적으로 실행되므로 동시 처리 성능이 매우 떨어진다.
mysql에서 SELECT .. FOR UPDATE/SHARE
쿼리는 레코드에 각각 쓰기/읽기 잠금을 걸고 접근한다. 하지만 다른 격리 레벨들에서는 그냥 SELECT
는 아무 락도 걸지 않고 접근한다.
하지만 SERIALIZABLE 레벨에서는 그냥 SELECT도 읽기 락을 걸고 레코드에 접근한다. 이렇게 읽기 잠금이 걸릴 레코드에는 다른 트랜잭션이 추가, 수정, 삭제 등을 할 수가 없다.
SERIALIZABLE은 안전하지만 성능이 떨어지는 방법이므로, 극단적으로 안전한 작업이 필요한 상황이 아니라면 잘 사용하지 않는다.