트랜잭션 ACID Isolation
트랜잭션은 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법이다. 트랜잭션은 전체가 성공(commit) 하거나 실패(abort) 한다(p222).
트랜잭션 상태에 대해 좀 더 자세히 보자. 트랜잭션이 시작되고(Active) 진행중에 실패하면 Failed 상태가 되고 롤백이 다 끝난 후 Aborted 가 된다. 그리고 데이터를 persist 하는 중(commit 진행중)에 실패하면 Failed 상태가 된다.
ACID Isolation
다음은 ACID Isolation 에 대해서 살펴보자. '격리시킨다' 는 말의 의미가 무엇인지 이해하려면 애플리케이션 보다 db 관점에서 봐야한다. 아래 그림처럼 여러 트랜잭션들이 동시에 실행중인 상황에서, interleaving 하는 트랜잭션들을 어떻게 conflict 없이 스케줄링 할거냐가 중요하다.
'interleaving 하는 트랜잭션' 의미는 여러 트랜잭션들이 동시에 실행되면서, 각 트랜잭션 연산들이 시간적으로 서로 교차하며 실행되는 것을 뜻한다. 하지만 외부적으로는 db 에서 실행되는 유일한 트랜잭션인 것처럼 보이게 하는걸 격리시킨다고 이해하자.
더나은 performance 를 위해 동시성 처리는 필수다. 하지만 serial equivalence(serializability) 를 지킬 수 있게 스케줄링 해야한다. 다시말해 트랜잭션들이 병렬로 교차해서 실행되지만 최종적인 결과는 순차적으로(독립적으로) 실행된 것과 같은 결과가 나와야 한다.
트랜잭션들을 병렬로 실행하면 다양한 conflict 가 발생할 수 있다. 하나씩 살펴보고 conflict 해결에 필요한 isolation level 에 대해서도 알아보자.
cf) Real MYSQL 표현에 따르면 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는게 isolation level 이다.
1. read uncommitted(dirty read) 문제
T2 는 T1 에서 commit 되지 않은 A 를 읽는다. 그리고 새롭게 14로 update 하고 commit 까지 했는데 T1 이 abort 되버렸다. 사실 read uncommitted 는 거의 사용하지 않는 isolation level 이다.
데이터 중심 애플리케이션 설계 책에 있는 dirty read 케이스를 보자. user1 트랜잭션이 commit 되기 전 user2 트랜잭션이 read 하면서 읽지 않은 메세지와 읽지 않은 메시지 개수가 안맞게 됐다. dirty read 로 인해 부분적으로 갱신된 상태의 db 를 볼 수 있다.
2. unrepeatable read(read skew) 문제
T1 은 같은 트랜잭션에서 A 를 read 할 때 처음과 두번째 결과가 달라진다. 그런데 이 문제도 read uncommitted, 즉 T2 가 commit 되지 않은 T1 A 를 읽었기 때문에 발생하는거 아니냐고 할 수 있다.
하지만 read committed isolation level 이어도 unrepeatable read 문제는 발생할 수 있다.
데이터 중심 애플리케이션 설계 책에 있는 unrepeatable read 케이스를 보자. Alice 는 Account1, Account2 각각에 500 달러씩 갖고 있다. 그런데 Account2 에서 Account1 로 100 달러를 이동시키는 중에 Alice 가 계좌를 조회하면, 900 달러만 있다고 결과가 나올 수도 있다.
Alice 가 조회했던 계좌 잔고들은 그 시점엔 commit 된 상태였다. 다시말하면 read committed isolation level 이어도 unrepeatable read 문제는 발생할 수 있다.
Alice 문제는 일시적인 현상이고, unrepeatable read isolation level 에서 이런 부정합 현상은 일반적인 웹 애플리케이션에서는 큰 문제가 아닐 수도 있다(Real MySQL). 하지만 어떤 상황에서도 일관성을 지켜야 하는 애플리케이션도 있다.
repeatable read(postgresql and mysql) / snapshot isolation / serializable(oracle)
데이터 중심 애플리케이션 설계 책에서는 snapshot isolation 이라고 부른다. SQL 표준에 snapshot isolation 개념이 없고 snapshot isolation 과 비슷해 보이는 repeatable read 는 SQL 표준에 정의되어 있어서, postgresql 과 mysql 이 repeatable read 로 부른다고 설명한다.
repeatable read 는 Real MySQL 책 설명이 더 잘되어 있는거 같아서 먼저 설명한다. repeatable read 는 mysql innoDB 스토리지 엔진에서 기본으로 사용하는 격리 수준이다. MVCC (Multi Version Concurrency Control) 을 위해 언두 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있게보장한다.
모든 InnoDB 트랜잭션은 고유한 트랜잭션 번호를 가지며 언두 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션 번호가 포함돼 있다. 아래 그림을 보면 트랜잭션 10 에서 select 쿼리는, 트랜잭션 12 에서 값을 변경시키고 commit 까지 해도 동일한 이전 결과를 보여준다. 트랜잭션 10 안에서 실행되는 모든 select 쿼리는 트랜잭션 번호가 10(자신의 트랜잭션 번호) 보다 작은 트랜잭션 번호에서 변경한 것만 보게 된다.
이제 데이터 중심 애플리케이션 설계 책 예제를 보자. 트랜잭션 13이 update 다하고 commit 까지 했지만 트랜잭션 12 는 이전 결과로 조회된다.
3. phantom read(phantom row) 문제
repeatable read isolation level 은 phantom read 문제가 생길 수 있다. Real MySQL 책 내용으로 설명하겠다. 사용자 B 는 'select ... for update' 쿼리로 조회한다. 'select ... for update' 쿼리는 select 하는 레코드에 쓰기 lock 을 걸어야 한다. 문제는 언두 레코드에는 lock 을 걸 수 없다. 그래서 'select ... for update' 쿼리로 조회되는 레코드는 언두 영역의 변경 전 데이터를 가져오는게 아니라 현재 레코드 값을 가져온다.
마지막으로 남은 serializable isolation level 은 동시성이 중요한 db 는 거의 사용하지 않는다. 한 트랜잭션에서 읽고 쓰면 다른 트랜잭션에서는 절대 접근할 수 없는 가장 강력한 동시성 제어다.
4. lost update 문제
T1 입장에서는 A 가 20이 되어버리고, T2 입장에서는 B 가 40이 되어 버리는 문제다.
데이터 중심 애플리케이션 설계 책 예제를 살펴보자. 두 user 가 counter 를 동시에 증가시켰지만 race condition 때문에 44가 아닌 43이 됐다.
두 트랜잭션이 read-modify-write 작업(db에서 값을 읽고 변경 후 다시 쓰기)을 동시에 하면 두 번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실 될 수 있다.