ACID는 원자성, 일관성, 고립성, 지속성을 나타낸다
이는 데이터베이스 시스템의 네 가지 중요한 속성이다
트랜잭션은 하나의 작업 단위로 묶이는 SQL 쿼리의 모음이다
여러 쿼리를 하나의 작업 단위로 묶는 이유는 SQL의 특성 때문이다
데이터베이스의 데이터는 구조화되어 있고 여러 테이블을 가지고 있다
그래서 하나의 쿼리에서 원하는 모든 작업을 수행하는 것은 매우 어렵고 불가능할 때도 있다
그래서 응응 프로그램에서 논리적으로 원하는 것을 달성하기 위해 한 개 이상의 쿼리를 실행해야 한다
예를 들면 계좌 입금 과정이 있다
이것은 세 가지 다른 쿼리로 이루어진 트랜잭션이다
트랜잭션은 새로운 트랜잭션을 시작하겠다는 BEGIN으로 시작된다
트랜잭션에 작성한 쿼리는 COMMIT하기 전까지 디스크에 영구적으로 저장되지 않는다
또한 예기치 못한 종료에 대비해서 트랜잭션을 하기 전으로 돌리는 ROLLBACK이 있다
보통 트랜잭션은 데이터를 변경하고 수정하는 데 사용한다
하지만 읽기 전용 트랜잭션을 가질수도 있다
읽기 전용 트랜잭션 없이 각 쿼리를 이용해 읽으면 일관성이 유지되지 않을 수도 있다
예를 들어 보고서를 생성하고 거래시간을 기준으로 일관된 스냅샷을 얻고자 할 때 읽기 전용 트랜잭션을 사용하면 고립성이 보장된다
트랜잭션은 항상 시작된다
만약 트랜잭션을 시작하지 않으면, 데이터베이스에서 대신 시작한다
원자성은 트랜잭션이 하나의 작업 단위이며 나눌 수 없다는 개념이다
따라서 100개의 쿼리가 포함된 트랜잭션이 있다면, 이 100개 쿼리가 모두 성공해야 한다
중간에 하나라도 실패핝다면, 성공한 모든 쿼리는 롤백되어야 한다
데이터베이스에 여러 연결이 있고 각각 트랜잭션을 실행하면, 여러 트랜잭션이 동시에 동일한 데이터를 쓰거나 읽으려고 경합하는 동시성이 발생할 수 있다
여기서 고립성이 필요하다
현재 진행 중인 트랜잭션이 아니라 다른 곳에서 먼저 완료된 트랜잭션에서 발생한 변경 사항을 볼 수 있는지 알아야 한다
이 변경점을 볼 수 있는 것은 상황에 따라 다르고, 그 결과로 여러 가지 읽기 현상(Read Phenomena)
이 나타난다
다음과 같은 현상들이 있고, 대부분 이런 현상을 피하려고 한다
Dirty Reads란 현재 실행 중인 트랜잭션에서 발생한다
다른 트랜잭션에서 쓴 내용을 읽지만 아직 커밋되지 않은 것들을 읽는다
그래서 방금 읽은 변경된 내용이 롤백될 수도 있고, 커밋될 수도 있고, 데이터베이스가 충돌날 수도 있다
Dirty는 쓴 내용이 완전히 flush되지 않았거나 완전히 커밋되지 않았다는 것을 의미한다
Non-repeatable reads는 트랜잭션 중에 값을 읽은 후, 동일한 트랜잭션에서 다시 그 값을 읽었는데 값이 변경되는 경우를 말한다
Phantom reads는 아직 현재 트랜잭션에는 존재하지 않는 행이어서 실제로 읽을 수 없지만, 다른 트랜잭션에 행 추가로 결과에는 추가가 되서 보인 경우를 말한다
Lost Updates는 내가 어떤 행을 업데이트 한 후 다시 읽으려고 하는데, 읽기 전에 다른 트랜잭션에서 그 행을 변경해서 내가 쓴 값이 사라지는 현상을 말한다
다음과 같은 SALES 테이블이 있다고 가정한다
PID | QNT | PRICE |
---|---|---|
Product 1 | 10 | $5 |
Product 2 | 20 | $4 |
여기서 아래 그림과 같이 두 개의 트랜잭션이 병렬로 실행된다고 가정한다
PID | QNT | PRICE |
---|---|---|
Product 1 | 15 | $5 |
Product 2 | 20 | $4 |
원래 기대했던 결과는 $130이었다
이렇게되면 오류는 아니지만 같은 트랜잭션 내에서 일관성이 없어진다
심지어 TX2의 결과는 커밋되지 않았는데도 그 값을 TX1에서 읽었다
TX1에서 더티 값을 읽었고 이 값은 사라졌고, 이것은 큰 문제가 발생할 수 있다
위 예시와 같이 SALES 테이블이 있다고 가정한다
PID | QNT | PRICE |
---|---|---|
Product 1 | 10 | $5 |
Product 2 | 20 | $4 |
다음과 같이 트랜잭션을 진행한다
Dirty Read와 다른 점은 TX2에서 커밋을 진행한 점이다
커밋을 진행했으므로 TX1에서 변경된 값을 읽어도 더티한 값이 아니다
그러나 우리가 기대한 결과는 $130이 아니라, $155인 것이다
이렇게 똑같은 값에 대해 결과가 다른 것을 Non-repeatable read라고 한다
Non-repeatable read
는 항상 피해야 하는 것은 아니다
무엇을 구축하려고 하는지에 따라서 이 문제는 괜찮다고 할 수도 있다
Phantom Read는 범위 쿼리에서 발생한다
PID | QNT | PRICE |
---|---|---|
Product 1 | 10 | $5 |
Product 2 | 20 | $4 |
이번에도 위 SALES 테이블에서 다음 순서로 트랜잭션이 진행된다
TX1의 1번 쿼리의 결과로 2개의 데이터만 읽는다
하지만 TX2의 트랜잭션 결과로 SALES 테이블은 다음과 같이 데이터가 추가된다
PID | QNT | PRICE |
---|---|---|
Product 1 | 10 | $5 |
Product 2 | 20 | $4 |
Product 3 | 10 | $1 |
그 후 TX1의 3번 쿼리의 결과로 $140을 얻게 된다
이는 TX1의 진행 중에는 알 수 없는 유령 같은 데이터가 추가가 되서 결과가 집계된 현상이다
PID | QNT | PRICE |
---|---|---|
Product 1 | 10 | $5 |
Product 2 | 20 | $4 |
SALES 테이블에서 아래 트랜잭션을 병렬로 실행한다
TX1의 1번 쿼리의 결과로 SALES 테이블의 결과는 다음과 같아진다
PID | QNT | PRICE |
---|---|---|
Product 1 | 20 | $5 |
Product 2 | 20 | $4 |
하지만 TX2의 2번 쿼리의 결과는 다음과 같다
PID | QNT | PRICE |
---|---|---|
Product 1 | 15 | $5 |
Product 2 | 20 | $4 |
Product 1의 QNT 값이 25가 아닌 15가 되었다
이는 TX2도 실행시점에는 SALES 테이블의 Product 1의 QNT 값을 10으로 읽고 나서 실행했기 때문이다
따라서 TX1의 3번 쿼리의 결과로 $180을 기대했지만 실제로는 $155이 결과로 나온다
이렇듯 다른 트랜잭션에 의해서 먼저 업데이트한 값이 덮어씌워졌기 때문에 Lost Update라고 한다
다음 고립 수준들은 위에서 본 읽기 현상을 해결하기 위해 개발 됐다
고립성이 없다
외부에서 변경된 모든 사항은 커밋 여부와 상관 없이 현재 트랜잭션에서 보여진다
커밋되지 않은 값들을 읽을 수 있으므로 Dirty read
가 발생할 수 있다
트랜잭션 내의 각 쿼리는 다른 트랜잭션에서 커밋된 변경 사항만 볼 수 있다
따라서 다른 트랜잭션이 변경을 수행하는 동안 커밋하지 않으면, 현재 트랜잭션에서는 변경 사항을 볼 수 없다
이 고립 수준은 많은 데이터베이스에서 사용하고 있다
Non-repeatable read
를 해결하기 위해 만들어진 고립 수준이다
Repeatable Read
는 읽기를 반복 가능하게 만드는 고립 수준이다
동일한 트랜잭션 내에서 한 번 읽은 값은 다시 그 값을 읽어도 변경되지 않는다는 것을 의미한다
따라서 트랜잭션은 실행 중인 동안에 쿼리가 행을 읽을 때, 해당 행이 변경되지 않도록 보장한다
하지만 Phantom Read
를 없애지 않는다
각 쿼리는 트랜잭션의 시작 시점까지 커밋된 변경 사항만 볼 수 있다
이것은 트랜잭션 시작 직전의 전체 데이터베이스의 스냅샷과 같다
이는 모든 읽기 현상을 제거하는 것을 보장한다
물리적으로 트랜잭션이 데이터베이스에 연이어 직렬화된 것처럼 구현된다
더 이상 동시성이 없다
Serializable
에서는 데이터베이스가 여러 트랜잭션의 진행되는 순서를 결정하여 거의 동일한 결과를 얻을 수 있도록 한다
각 데이터베이스시스템마다 고립 수준을 다르게 구현한다
lock
을 이용한다. 행 레벨 잠금, 페이지 잠금, 테이블 잠금을 이용한다lock
을 사용하지 않는다. 변경을 추적하다 트랜잭션이 서로 충돌하면 트랜잭션을 실패시킨다Repetable Read
는 lock을 이용해 행을 잠근다Serializable
은 적극적인 동시성 제어로 구현된다. 실제로 직렬화를 하면 데이터베이스가 너무 느려진다일관성은 두 가지로 나뉜다
실제로 디스크에 있는 것과 데이터 모델이 일치하는지 확인해야 한다
데이터가 디스크에서 일관성을 유지할 수 있지만, 데이터의 읽기는 여러 인스턴스가 동기화되지 않아 일관성이 없어질 수 있다
데이터 일관성을 위해 다음 조건을 만족해야 한다
하나의 계좌에서 다른 계좌로 100달러를 옮기는 도중에 데이터베이스가 다운되고 다시 시작했을 때 이것이 롤백되지 않으면 데이터 불일치가 일어난다
따라서 데이터 일관성을 위해 원자성이 필요하다
고립성 역시 데이터의 불일치로 이어질 수 있다
같은 Isoloation Level
에 따라 읽기를 요청하면 같은 데이터를 얻을 수 있지만, 다른 Isolation Level
로 읽기를 요청하면 고립 수준에 따라 다른 결과가 나타날 수 있으며 이는 데이터 불일치 결과로 이어질 수 있다
다음은 참조 무결성을 만족한 예시다
Pictures
테이블에서 받은 좋아요 수의 합과 Picture_Likes
테이블에 있는 좋아요 수의 합이 동일하다
이것이 바로 데이터의 일관성이다
다음은 참조 무결성을 만족하지 않아 데이터의 일관성이 없는 예시다
일관성은 일관된 읽기도 만족해야 한다
위 예시처럼 값을 X로 업데이트 하면 데이터베이스는 값 X를 유지해야 한다
이 작업이 커밋된 후 다음 읽기를 진행하면 값 X를 반환해야 한다
이것이 바로 일관된 읽기다
만약 데이터베이스에서 변경 사항을 커밋한 후에 즉시 읽기를 실행해서 변경 사항이 보이지 않는다면, 실제로 일관성이 없는 것이다
데이터베이스가 여러 복제본을 가지고 있다면 읽기 불일치가 발생할 수 있다
변경 사항을 주 데이터베이스에 쓰면, 주 데이터베이스는 변경 사항을 복제본에 동기화할 것이다
그 때, 변경 사항이 복제본에 동기화 되기 이전에 복제본에서 읽으면 이전 값을 얻을 수도 있다
이것이 여기서 말하는 불일치다
지금은 일관성이 없지만, 결국 일관성을 갖게 될 것이다라는 의미다
데이터의 일관성에서, 데이터가 손상되고 참조 무결성이 깨진 경우에는 궁극적 일관성이 나타나지 않는다
궁극적 일관성이라고 말하는 것은 읽기에만 해당된다
계속해서 읽기를 요청하면 이전 값을 얻어도 결국에는 정확한 값을 얻게 될 것이다를 의미한다
지속성은 트랜잭션이 커밋될 때 변경 사항을 비휘발성 저장소에 저장해 지속하는 능력이다
지속성 기술은 다음과 같다
WAL
은 쓰기 전용 로그다
모든 변경 사항은 먼저 여기에 기록된다
그리고 이것을 디스크에 flush
해 지속성을 보장한다
만약에 충돌이 발생한다면 모든 WAL
항목을 다시 읽고 상태를 효과적으로 다시 구축할 수 있다
또 다른 방법으로는 비동기 스냅샷이 있다
쓰는 작업을 모두 메모리에 유지한 후 백그라운드에서 비동기적으로 한꺼번에 디스크에 스냅샷한다
AOF
는 Redis에서 사용하는 WAL과 비슷한 저장 방법이다
변경사항을 추적하고 기록한다
모든 변경 사항을 디스크에 직접 쓰는 건 비용이 많이 든다 (인덱스, 데이터 파일, 열 행 등)
더 최적화된 방법이 필요하다
그래서 이러한 변경 사항의 버전을 압축해서 WAL 세그먼트로 만들어 이 로그 세그먼트만을 기록한다
WAL 내용은 단순히 어떤 값을 어떤 값으로 변경했다는 내용이 들어있다
데이터베이스가 디스크에 쓰도록 OS에 요청을 하면, OS는 디스크에 쓰지 않고 메모리 캐시에 쓴다
데이터베이스가 OS에게 WAL 세그먼트를 디스크에 기록하라고 요청하면, OS는 캐시에 기록하고 성공적으로 기록했다고 데이터베이스에게 알려준다
하지만 실제로는 캐시에만 기록하고 실제로 디스크에 기록하지는 않고 성능을 위해 일괄 처리를 할 것이다
문제는 여기서 OS 충돌이 발생하면 데이터가 손실될 수 있다
그래서 데이터베이스에는 OS 캐시를 쓰지 않고 바로 변경 사항을 디스크에 flush
요청하는 명령이 있다
이것을 Fsync 명령이라고 한다
Fsync
명령은 항상 쓰기를 강제로 이루어지게 하는데 비용이 많이 들고 성능을 저하시킬 수 있다