7장 트랜잭션
- 데이터베이스 SW or HW는 언제든지 실패할 수 있다.
- 애플리케이션은 언제라도 죽을 수 있다.
- 네트워크가 끊기면 애플리케이션과 데이터베이스의 연결이 갑자기 끊기거나 통신이 안될 수 있다.
- 여러 클라이언트가 동시에 데이터베이스에 쓰기를 실행해서 다른 클라이언트가 쓴 내용을 덮어쓸 수 있다.(LWW)
- 클라이언트가 부분적으로만 갱신돼서 비정상적인 데이터를 읽을 수 있다.
- 클라이언트 사이 경쟁 조건은 예측하지 못한 버그를 유발할 수 있다.
트랜잭션은 이런 문제를 단순화하기 위해 전체가 성공(커밋), 실패(롤백)한다.
동시성 제어 분야에서는 여러 경쟁 조건
- 커밋 후 읽기(read committed)
- 스냅숏 격리(snapshot isolation)
- 직렬성(serializability)
가 존재한다.
애매모호한 트랜잭션 개념
데이터베이스마다 구현하는 트랜잭션의 강도가 다르기 때문에 트랜잭션 개념이 조금 애매모호하다. 어디서는 느슨한 트랜잭션, 어디서는 강도 높은 트랜잭션을 구현한다.
ACID 의미
- 원자성(Atomicity)
- 일관성(Consistency)
- 격리성(Isolation)
- 지속성(Durability)
ACID 구현은 제각각이며 격리성에 모호함이 있다.
원자성
원자적이란 쪼갤 수 없는 것을 의미하지만, 대충 어보트 능력(abortability) 정도.
일관성
= 좋은 상태
격리성
한 트랜잭션의 작업은 다른 트랜잭션에 의해 영향을 받으면 안된다.
직렬성 격리 같은 경우엔 성능 오버헤드로 인해 거의 사용되지 않음
- 한번에 하나의 스레드만 작업할 수 있기 때문에 동시성을 해치게 된다.
팬텀 리드, 더티 리드 등 다른 트랜잭션에 의해 영향을 받을 수 있음.
혹은 동시 접근으로 인해 동시성 문제가 발생할 수도 있음.
지속성
한번 쓴 데이터는 비휘발성 저장소에 기록되어 잃어버리지 않아야 한다.
단일 객체 연산과 다중 객체 연산
- 원자성
- 격리성: 서로 방해하지 말아야 하고, 다른 트랜잭션은 그 내용을 모두 보던지 안 보던지 둘 중 하나만 해야 한다. 즉 일부분만 보이거나 하는 등 X
원자성, 격리성 두개에 대해 나오는 듯?
단일 객체 쓰기
원자성과 격리성은 단일 객체를 변경하는 경우에도 적용된다.
- 일부분 데이터만 전달 받은 상태에서 네트워크 연결이 끊긴 경우, 이 데이터를 저장할 것인지?
- 데이터베이스가 디스크에 새 데이터를 덮어쓰는 도중에 전원이 나갈 경우 기존 값과 새 값은 공존하게 될까?
- 문서를 쓰고 있을 때 다른 클라이언트가 그 문서를 읽으면 부분 갱신된 값을 읽게 될까?
이 문제들은 혼란스럽기 때문에 저장소 엔진들은 거의 보편적으로 한 노드에 존재하는 단일 객체 수준에서 원자성/격리성 제공을 목표로 한다.
원자성은 장애 복구 로그를 통해 구현할 수 있고, 격리성은 각 개체 잠금을 사용해 구현할 수 있다.
어떤 데이터베이스는 compare-and-set 연산을 제공해 read-modify-write 주기 반복을 없앤다.
- 즉, 데이터를 쓰기 전에 해당 데이터가 누군가에 의해 변경됐는지를 체크하고, 변경되지 않았을 경우에만 쓰기를 허용한다.
- 이 기능은 갱신 손실(lost update)를 방지하므로 유용하다.
다중 객체 트랜잭션 필요성
많은 분산 데이터스토어는 다중 객체 트랜잭션 지원을 포기했다. 왜냐하면 여러 파티션에 걸쳐 구현하기 어렵고 매우 높은 가용성과 성능이 필요한 곳에서는 방해가 되는 시나리오도 있기 때문이다.
오류와 어보트 처리
트랜잭션의 핵심은 어보트되고 안전하게 재시도가 가능한 것.
여기서 오류 복구는 애플리케이션에게 책임이 있다. 어보트된 트랜잭션을 재시도하는 것은 간단하고 효율적인 오류 처리 메커니즘이지만 완벽하진 않다.
- 트랜잭션이 성공했지만, 커밋 성공을 알리기 전 네트워크가 끊기고, 애플리케이션은 모르고 재시도하는 경우(중복 제거 메커니즘이 없다면)
- 오류가 과부하 때문이라면 재시도는 문제를 악화시킬 수 있다.(재시도 횟수 제한 고려)
- 일시적 오류만 재시도 가치가 있음(영구 오류라면 의미 없음)
- 트랜잭션이 데이터베이스 외부에 부수 효과가 있는 경우 (이메일은 이미 전송됐는데, 트랜잭션을 재시도했다고, 이메일이 또 재전송되길 바라지 않음)
- 클라이언트 프로세스가 재시도 중 죽어버리면 쓰기 데이터가 모두 손실된다.
완화된 격리 수준
- 두 트랜잭션이 서로 의존하지 않으면 안전하게 병렬 실행이 가능하다.
- 동시성 문제는 동일한 데이터에 대해 동시에 쓰려고 할 경우에만 나타난다.
커밋 후 읽기(read committed)
- 데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다(더티 읽기 X)
- 데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다(더티 쓰기 X)
더티 읽기 방지

아직 커밋되지 않은 쓰기 데이터를 읽는 것.
커밋 후 읽기 격리 레벨에서는 더티 리드를 막아야 한다. 더티 리드를 막았을 때 유용한 이유
- 다른 트랜잭션이 잘못된 값으로 인해 잘못된 결정을 하지 않게 한다.
- 트랜잭션이 롤백되면, 다른 트랜잭션은 결코 커밋되지 않을 데이터를 볼 수 있게 된다.
더티 쓰기 방지
두 트랜잭션이 동일한 객체를 동시에 쓰려고 하면 나중에 쓴 내용이 먼저 쓴 내용을 덮어쓰게 된다.
- 이를 더티 쓰기(dirty write)라고 함.
더티 쓰기를 막음으로써 해당 문제를 해결한다.

- 트랜잭션들이 여러 객체를 갱신하면 더티 쓰기는 나쁜 결과를 유발할 수 있음.
- 커밋후 읽기는 동시성 문제에서 경쟁 조건을 막지는 못한다.
커밋 후 읽기 구현
매우 널리 쓰이는 격리 수준이며, 가장 흔하게 로우 수준 잠금을 사용해 더티 쓰기를 방지할 수 있다.
트랜잭션에서는 쓰기를 하기 전에 해당 로우에 대한 잠금을 획득하고, 트랜잭션이 커밋/어보트 될 때까지 잠금을 보유하고 있어야 한다.
더티 읽기도 동일하게 잠금을 사용해 구현할 수 있지만, 읽기만 실행하는 여러 트랜잭션들이 쓰기 트랜잭션이 완료되길 기다려야 할 수 있기 때문에 현실적으로 잘 사용되진 않는다.
스냅숏 격리와 반복 읽기
커밋 후 읽기를 사용하더라도 동시성 버그가 생기는 경우는 존재한다.

비반복 읽기(nonrepeatable read)나 읽기 스큐(read skew)가 있다.
스냅숏 격리는 이 문제의 가장 흔한 해결책이다. 각 트랜잭션은 일관된 트랜잭션으로 부터 읽는다.
- 중간에 데이터가 다른 트랜잭션에 의해 변경되더라도, 트랜잭션은 특정한 시점의 과거 데이터를 본다.
스냅숏 격리는 백업이나 분석처럼 오래 걸리며 읽기만 실행하는 질의에 요긴할 수 있다.
Postgresql, innoDB 저장소 엔진을 쓰는 mysql, oracle, sql 서버 등에서 지원됨.
스냅숏 격리 구현
더티 쓰기를 방지하기 위해 쓰기 잠금을 사용하며, 읽기 작업에 대한 차단을 하지 않는다.
데이터베이스는 커밋된 버전 여러개를 유지할 수 있기 때문에 다중 버전 동시성 제어(MVCC)라고도 한다.

해당 로우를 삽입/삭제 요청을 한 트랜잭션 ID를 저장하며, 삭제 요청 로우에 대해서 접근하지 않음이 확실해지면 데이터베이스 가비지 컬렉션 프로세스가 해당 로우들을 실제 삭제한다.
일관된 스냅숏을 보는 가시성 규칙
트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 판단한다.
- 읽기를 실행하는 트랜잭션이 시작한 시점에 읽기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태이다.
- 읽기 대상 객체가 삭제된 것으로 표시되지 않았다. 또는 삭제된 것으로 표시됐지만 읽기 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 아직 커밋되지 않았다.
색인과 스냅숏 격리
다중 버전 데이터베이스에서 색인은 단순하게 객체의 모든 버전을 가리키고, 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내게 하는 것이다.
반복 읽기와 혼란스러운 이름
오라클은 직렬성, 포스트그레스큐엘, mysql은 반복 읽기(repeatable read)라고 하는데, 초창기엔 스냅숏 격리 개념이 없었기 때문이다.
갱신 손실 방지
동시에 데이터베이스 쓰기를 할 경우 발생할 수 있는 또 다른 문제는 갱신 손실(lost update)이며 동시에 쓸 경우 먼저 쓴 데이터가 손실될 수 있다. (LWW)
갱신 손실 방지의 해결책
원자적 쓰기 연산
커서 안정성(cursor stability)라고도 부르는데, 갱신이 적용될 때 다른 트랜잭션에서 그 객체를 읽기 못하게 데이터베이스에서 원자적 연산을 제공해주는 것이다.
명시적인 잠금
애플리케이션에서 명시적으로 갱신할 객체를 잠그는 것.

갱신 손실 자동 감지
read-modify-write 주기가 순차적으로 실행되도록 해서 갱신 손실을 방지하는 것이다.
대안으로 병렬 실행을 허용하고, 트랜잭션 관리자가 갱신 손실을 발견하면 어보트 시키고 재시도하게끔 하는 방법.
- 어떠한 신경도 안 써도 자동으로 되기 때문에 매우 좋은 기능임.
mysql/innoDB는 갱신 손실 감지를 하지 않는다.
Compare-and-set

값을 마지막으로 읽은 후 해당 객체에 변경이 이루어졌는지 확인하고, 변경이 없었다면 갱신되게 한다. (갱신 손실 회피)
충돌 해소와 복제
복제 데이터베이스 환경에서는 최신 복사본이 여러개 존재할 수 있기 때문에
잠금과 compare-and-set 기법은 사용할 수 없다(최신 복사본이 하나라고 가정하기 때문에)
반면에 원자적 연산은 복제 상황에서도 잘 동작하고, 최종 쓰기 승리(LWW)는 갱신 손실이 발생하기 쉽지만, 많은 복제 데이터베이스는 LWW가 기본 설정이다.
쓰기 스큐와 팬텀
_다른 트랜잭션들이 동시에 같은 객체에 쓰려고 할 때 발생할 수 있는 경쟁조건
- 더티 쓰기
- 갱신 손실
-
쓰기 스큐

- 최소한 한 명의 의사가 대기해야 하는 시스템에서 두 트랜잭션이 모두 커밋되면 대기하는 의사가 한명도 없게 됨
쓰기 스큐를 특징짓기
더티 쓰기 / 갱신 손실
- 다른 트랜잭션이 하나의 동일한 객체를 갱신할 때 발생 가능
쓰기 스큐
- 두 트랜잭션이 같은 객체들을 읽어서 그중 일부를 갱신할 때 발생 가능
- 다른 트랜잭션은 다른 객체를 갱신
- 쓰기 스큐 막기
- 원자적 단일 객체 연산으로 해결 X
- 갱신 손실 자동 감지도 도움 X
- 여러 객체와 연관된 제약 조건 설정 → 트리거/구체화 뷰(materialized view) 사용하여 구현 가능
- 직렬성 격리 수준을 사용할 수 없다면 트랜잭션이 의존하는 로우를 명시적으로 잠그기

추가적인 쓰기 스큐의 예
- 회의실 예약 시스템
- 다른 사용자가 동시에 충돌되는 회의 삽입을 막을 수 없음 → 직렬성 격리 필요

- 다중 플레이어 게임
- 플레이어들이 두 개의 다른 물체를 게임판 위의 같은 위치로 옮기거나 규칙을 위반하는 다른 이동을 막을 수 없음 → 규칙에 따라 유일성 제약 사용 가능
- 사용자명 획득
- 웹사이트에서 두 명의 사용자가 동시에 같은 사용자명으로 계정 생성 시도 → 유일성 제약 조건으로 해결
- 이중 사용 방지
- 두 개의 지불 항목이 모여서 잔고가 음수가 되게 하는 경우 발생 가능
쓰기 스큐를 유발하는 팬텀
phantom : 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과
- 1단계 : SELECT로 특정 검색 조건에 부합하는 로우를 검색하여 요구사항 만족하는지 판단
- 2단계 : 1단계 질의 결과에 따라 연산을 계속 처리할지 오류 보고/중단할지 결정
- 3단계 : 계속 연산을 처리하는 경우에 INSERT, UPDATE, DELETE 트랜잭션을 커밋
- 의사 대기 예시 → 1단계의 로우를 SELECT … FOR UPDATE로 잠궈서 쓰기 스큐 회피 가능
- 나머지 예시 → 검색 조건에 부합하는 로우가 존재하지 않으면 쓰기 작업이 실행되는 구조이므로 1단계 질의가 아무 로우도 반환하지 않아서 SELECT … FOR UPDATE 사용 불가
충돌 구체화
materializing conflict : 구체화 뷰를 사용해서 팬텀을 해당 뷰의 로우 집합에 대한 잠금 충돌로 변환
- 회의실 예약 시스템에서 회의실과 시간 범위의 모든 조합에 대해 로우를 미리 만들어 둠
- 원하는 회의실과 시간 범위에 해당하는 로우를 SELECT … FOR UPDATE로 잠금
- but, 어렵고 오류 발생 쉬움
- 동시성 제어 메커니즘이 애플리케이션 데이터 모델로 새어 나오는 것도 보기 좋지 않다??
- 따라서 다른 대안(직렬성 격리)이 불가능할 때 사용하는 최후의 수단
직렬성
여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장 → 가장 강력한 격리 수준
트랜잭션을 개별적으로 실행할 때 올바르게 동작한다면 동시에 실행할 때도 올바르게 동작할 것을 보장 → DB가 발생할 수 있는 모든 경쟁 조건을 막아줌
- 트랜잭션을 순차적으로 실행
- 2단계 잠금
- 낙관적 동시성 제어 (직렬성 스냅숏 격리)
실제적인 직렬 실행
동시성 문제를 피하는 가장 간단한 방법 : 한 번에 트랜잭션 하나만 직렬로 단일 스레드에서 실행
구현 가능 조건
- 트랜잭션이 접근해야 하는 모든 데이터가 램에 있어서 디스크에서 읽어 오기를 기다릴 필요 없음
- OLTP 트랜잭션은 짧고 실행하는 읽기/쓰기 개수 적음 vs OLAP 분석용 질의는 무겁고 읽기 전용
볼트DB/H 스토어/레디스/데이토믹에서 구현
CPU 코어 하나의 처리량으로 제한
트랜잭션을 스토어드 프로시저 안에 캡슐화하기
상호작용식 트랜잭션
- 애플리케이션이 질의를 실행하여 결과를 읽고, 첫 번째 질의 결과에 따라 다른 질의를 실행
- 애플리케이션과 DB 사이의 네트워크 통신에 많은 시간 소비
- 만약 DB에서 동시성을 허용하지 않고 한 번에 트랜잭션 하나씩만 처리하면 처리량 매우 낮음 (다음 질의까지의 대기시간이 길어지므로)
- ⇒ 단일 스레드에서 트랜잭션을 순차적으로 처리하는 시스템들은 상호작용하는 다중 구문 트랜잭션 허용 X
- 스토어드 프로시저 사용

스토어드 프로시저의 장단점
관계형 DB에서 사용 & SQL 표준이 됨
단점 (확장이 잘 되지 않는다!)
- DB 벤더마다 다른 스토어드 프로시저용 언어 (오라클 : PL/SQL, SQL 서버 : T-SQL, Postgre : PL/pgSQL) → PL/SQL 대신 범용 프로그래밍 언어 사용 (볼트DB : Java/Groovy, 데이토믹 : Java/Clojure, 레디스 : Lua)
- 애플리케이션 코드에 비해 DB 실행 코드는 디버깅 버렵고 버전 관리/배포 불편
- 잘못 작성된 SP가 코드단의 오류가 애플리케이션 서버에 미치는 것보다 DB에 훨씬 더 악영향 줄 수 있음
장점
- SP 사용 & 데이터 메모리 저장 → 모든 트랜잭션을 단일 스레드에서 실행 가능
- I/O 대기 불필요 & 다른 동시성 제어 메커니즘의 오버헤드를 회피하여 단일 스레드로 좋은 처리량 얻을 수 있음
볼트DB는 복제에도 SP 사용 ← 결정적이어야 함 (다른 노드에서 실행돼도 같은 결과가 나옴)
파티셔닝
직렬 실행은 DB의 트랜잭션 처리량이 단일 장비에 있는 단일 CPU 코어의 속도로 제한됨
여러 CPU 코어와 여러 노드로 확장하기 위해 파티셔닝 (볼트DB 지원)
- 각 트랜잭션이 단일 파티션 내에서만 읽고 쓰도록 파티셔닝 ← 단순한 키-값 데이터
- 여러 파티션에 접근해야 하는 트랜잭션은 모든 파티션에 걸쳐 코디네이션(직렬성 보장을 위해 모든 파티션에서 잠금을 획득한 다음 실행)을 해야 하며 이 경우 엄청나게 느리므로 유의 필요 ← 여러 보조색인이 있는 데이터
직렬 실행 요약
제약 사항
- 모든 트랜잭션은 작고 빨라야 함 (느린거 하나가 다른 트랜잭션 처리를 지연시키면 안됨)
- 활성화된 데이터셋이 디스크가 아닌 메모리에 적재될 수 있어야 함
- 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 낮아야 함
- 여러 파티션에 걸친 코디네이션이 필요하지 않도록 파티셔닝 해야 함
2단계 잠금(2PL)
two_phase locking, 2PL : 쓰기 트랜잭션은 다른 쓰기 트랜잭션 뿐만 아니라 읽기 트랜잭션도 허용하지 않으며 그 역도 성립 → 직렬성을 제공하여 모든 경쟁 조건으로 부터 보호
- 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰려고 시도하면 B가 진행되기 전에 A가 커밋되거나 어보트 되어야 함 (⇒ B가 A 몰래 객체 변경 안됨)
- 트랜잭션 A가 객체에 쓰고 트랜잭션 B가 그 객체를 읽으려고 하면 B가 진행되기 전에 A가 커밋되거나 어보트 되어야 함 (⇒ 객체의 과거 버전 읽기 안됨)
vs
스냅숏 격리 : 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 반대도 허용
2단계 잠금 구현
MySQL, SQL 서버 ⇒ 직렬성 격리 수준 구현에 사용
DB2 → 반복 읽기 격리 수준 구현에 사용
공유 모드(shared mode) vs 독점 모드(exclusive mode)
- 트랜잭션이 객체를 읽기 원하면 공유 모드로 잠금 획득 필요
- 트랜잭션이 객체를 쓰기 원하면 독점 모드로 잠금 획득 필요 (공유 모드든 독점 모드든 다른 어떤 트랜잭션도 동시에 잠금을 획득할 수 없다??)
- 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드 필요
- 트랜잭션이 잠금을 획득한 후에는 트랜잭션이 종료(커밋/어보트)될 때까지 잠금을 가지고 있어야 함
교착 상태 : A가 B의 잠금 해제 대기 & B가 A의 잠금 해제 대기 → DB가 자동으로 감지하여 트랜잭션 중 하나를 어보트 시키고 이 트랜잭션은 재시도
2단계 잠금의 성능
단점 (성능이 나쁘다!)
- 완화된 격리 수준을 쓸 때보다 트랜잭션 처리량과 질의 응답 시간이 크게 나빠짐
- 동시성 줄어듦, 잠금 획득/해제의 오버헤드 때문
- 관계형 DB들이 트랜잭션의 실행 시간을 제한하지 않으므로 경쟁이 있다면 매우 느려지거나 멈추게 할 수 있음
- 교착 상태가 자주 발생
서술 잠금
팬텀 문제(한 트랜잭션이 다른 트랜잭션의 검색 질의 결과를 바꿔버리는 문제)는 서술 잠금(predicate lock)으로 해결
- 특정 객체 (테이블 내의 한 row)에 속하지 않고 어떤 검색 조건에 부합하는 모든 객체에 속하는 경우, DB에 아직 존재하지 않지만 미래에 추가될 수 있는 객체(팬텀)에 적용 가능
-
ex. 회의실 예약

- 트랜잭션 A가 조건에 부합하는 객체를 읽으려면 공유 모드 서술 잠금이 필요, 다른 트랜잭션 B가 독점 잠금을 가지고 있으면 A는 B의 잠금 해제까지 대기 필요
- 트랜잭션 A가 객체 삽입/갱신/삭제를 시도할 때 부합하는 서술 잠금을 트랜잭션
색인 범위 잠금
진행 중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금을 확인하는데 오래 걸려서 잘 동작 X
색인 범위 잠금(index-range locking) = 다음 키 잠금(next-key locking)
- 더 많은 객체가 부합하도록 서술 조건을 간략하게 근사한 것
- 12시-1시 사이 123번 예약에 대한 서술 잠금 ≒ 모든 시간 범위의 123번 예약 잠금 or 12시 1시 사이의 모든 방 잠금
- room_id에 색인이 있는 경우 123번 예약이 있는지 확인하는데 색인 사용
- start_time / end_time에 색인이 있는 경우 12시-1시 사이의 예약이 있는지 확인하는데 색인 사용
- 쓰기 팬텀과 쓰기 스큐로부터 보호
- 서술 잠금보다 정밀하지는 않지만 오버헤드가 훨씬 낮음
- 색인이 없다면 테이블 전체에 공유 잠금 사용
직렬성 스냅숏 격리(SSI)
serializable snapshot isolation : 완전한 직렬성을 제공하면서도 스냅숏 격리에 비해 약간의 성능 손해만 있음
단일 노드 DB(포스트그레)와 분산 DB(파운데이션DB)에서 모두 사용
비관적 동시성 제어 대 낙관적 동시성 제어
2단계 잠금 = 비관적 동시성 제어
SSI = 낙관적 동시성 제어
- 트랜잭션이 커밋되기를 원할 때 격리가 위반되었는지 확인하여 어보트하고 재실행, 직렬로 실행된 트랜잭션만 커밋 허용
- 스냅숏 격리(트랜잭션에서 실행되는 모든 읽기가 DB의 일관된 스냅숏을 보게함)에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트시킬 트랜잭션을 결정하는 알고리즘 추가
- 단점 : 많은 트랜잭션이 같은 객체에 접근하려는 경쟁이 심하면 어보트 시켜야 할 트랜잭션의 비율이 높아져서 성능이 떨어짐
- 장점 : 예비 용량이 충분하고 트랜잭션 사이의 경쟁이 심하지 않으면 비관적 동시성 제어에 비해 성능이 좋음, 경쟁은 commutative(순서와 상관없이 결과 동일) 원자적 연산을 통해 줄일 수 있음
뒤처진 전제에 기반한 결정
트랜잭션은 어떤 전제 (현재 두명의 의사가 호출 대기 중)을 기반으로 동작을 하는데, 나중에 해당 트랜잭션이 커밋하려고 할 때 원래의 데이터가 바뀌어서 해당 전제가 참이 아닐 수 있음
따라서 직렬성 격리를 제공하려면 뒤처진 전제를 기반으로 동작하는 트랜잭션을 감지하고 어보트 시켜야함
- 오래된 MVCC 객체 버전을 읽었는지 감지 (읽기 전에 커밋되지 않은 쓰기가 발생함)
- 과거의 읽기에 영향을 미치는 쓰기 감지 (읽은 후에 쓰기가 실행됨)
오래된 MVCC 읽기 감지하기

트랜잭션 43이 읽을때는 (과거의) 스냅숏을 보지만 커밋하려고 할 때는 트랜잭션 42가 커밋된 상태이므로 읽을 때는 무시됐던 쓰기가 영향이 있어지고 트랜잭션 43의 전제가 유효하지 않음
→ 트랜잭션이 커밋하려고 할 대 DB가 무시된 쓰기 중 커밋된게 있는지를 추적/확인해야 하며 이 트랜잭션을 어보트 시킴
과거의 읽기에 영향을 미치는 쓰기 감지하기

트랜잭션 43이 커밋하려고 할 때 트랜잭션 42의 충돌되는 쓰기가 이미 커밋됐으므로 트랜잭션 43은 어보트됨
직렬성 스냅숏 격리의 성능
트레이드 오프는 얼마나 세밀하게 추적하느냐 → 어보트 비율에 좌우됨
정리
많은 경쟁 조건
- 더티 읽기 : 한 클라이언트가 다른 클라이언트가 썻지만 아직 커밋되지 않은 데이터를 읽음
- 더티 쓰기 : 한 클라이언트가 다른 클라이언트가 썻지만 아직 커밋되지 않은 데이터를 덮어씀
- 읽기 스큐(비반복 읽기) : 클라이언트가 다른 시점에 DB의 다른 부분을 봄 → 스냅숏 격리로 해결 by MVCC
- 갱신 손실 : 두 클라이언트가 동시에 read-modify-write 주기를 실행하여 한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 상태로 다른 트랜잭션이 쓴 내용을 덮어써서 데이터 손실 → 수동 잠금(SELECT … FOR UPDATE)로 해결
- 쓰기 스큐 : 트랜잭션이 읽은 값을 기반으로 결정을 한 뒤 해당 결정을 DB에 쓰지만 쓰기 시점에 결정의 전제가 참이 아닐 수 있음 → 직렬성 격리로 해결
- 팬텀 읽기 : 트랜잭션이 어떤 검색 조건에 부합하는 객체를 읽고 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 실행 → 색인 범위 잠금 필요
완화된 격리 수준
- 커밋 후 읽기 : 더티 읽기/더티 쓰기 방지 보장
- 스냅숏 격리(반복 읽기) : 각 트랜잭션이 DB의 일관된 스냅숏으로 부터 읽음
직렬성 격리
- 실제 트랜잭션을 순서대로 실행 : 트랜잭션의 실행 시간이 짧고 처리량이 낮을 때 효과적
- 2단계 잠금 : 성능 이슈 존재
- 직렬성 스냅숏 격리 : 낙관적 방법 사용 (커밋을 원할때에 확인해서 직렬적이지 않으면 어보트시킴)_