////
Search

7장 - 트랜잭션

Date
2025/01/17 09:00
Tags

1. 트랜잭션

현실 세계의 데이터 시스템은 여러 문제가 생길 수 있다.
시스템이 신뢰성을 지니려면 여러 결함을 처리해 전체 시스템의 치명적인 장애로 이어지는 것을 막아야한다.
트랜잭션 = 이런 문제를 단순화하는 메커니즘으로 채택돼 왔다.
트랜잭션은 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법
트랜잭션은 전체가 성공하거나 실패한다.
DB에 접속하는 애플리케이션에서 프로그래밍 모델을 단순화하려는 목적으로 만든것

2. 애매모호한 트랜잭션의 개념

현대의 대다수의 RDB는 트랜잭션을 지원한다.
NoSQL의 경우 RDB보다 훨씬 약한 보장의 트랜잭션을 의미한다.
트랜잭션은 이점과 한계가 있고 트레이드오프를 통해 운영 상황에 맞춰 조정한다.

2.1. ACID

데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.
ACID기본적으로 가용성을 제공하고, 유연한 상태를 가지며, 최종적 일관성을 지닌다는 뜻

2.1.1. 원자성(Atomicity)

일반적으로 원자적이란 더 작은 부분으로 쪼갤 수 없는 뭔가를 가르킨다.
하지만 ACID의 원자성은 동시성과 관련이 없다.
ACID의 원자성은 여러 쓰기 작업이 하나의 원자적 트랜잭션으로 묶인다면 모두 완료되거나 실패해야 한다.
오류가 생겼을 때 트랜잭션을 어보트하고 해당 트랜잭션에서 기록한 모든 내용을 취소하는 능력은 ACID 원자성의 결정적인 특징

2.1.2. 일관성(Consistency)

굉장히 여러 의미로 쓰인다.
복제 일관성과 비동기식으로 복제되는 시스템에서 발생하는 최종적 일관성
일관적 해싱은 어떤 시스템들에서 재균형화를 위해 사용하는 파티셔닝
일관성은 선형성을 의미한다.
ACID의 일관성은 항상 데이터가 진실이어야 한다는 것이다.
But 일관성의 아이디어는 애플리케이션의 불변식 개념에 의존한다.
원자성, 격리성, 지속성은 DB의 속성인 반면 일관성은 애플리케이션의 속성이기 때문에 C는 실제로 ACID에 속하지 않는다.

2.1.3. 격리성(Isolation)

ACID의 격리성은 동시에 실행되는 트랜잭션은 서로 격리된다는 것을 의미한다.
동시에 실행되는 트랜잭션은 서로 격리되어 방해할 수 없다.
한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼수 없든지 둘 중 하나여야 하고 일부분만 볼 수 있어서는 안된다.

2.1.4. 지속성(Durability)

데이터베이스 시스템의 목적은 데이터를 잃어버릴 염려가 없는 안전한 저장소를 제공하는 것이다.
지속성은 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장이다.

2.2. 단일 객체 연산과 다중 객체 연산

2.2.1. 단일 객체 쓰기

원자성과 격리성은 단일 객체를 변경하는 경우에도 적용된다.
그렇기에 저장소 엔진들은 거의 보편적으로 한 노드에 존재하는 단일 객체 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다.
원자성은 장애 복구용 로그를 써서, 격리성은 서로를 방해하지 않고, 각 객체에 잠금을 사용해 한 스레드만 객체에 접근하도록 구현할 수 있다.
누군가에 의해 바뀌지 않았을 때만(Compare and set) 쓰기가 반영되도록 만드는 경우도 있다.
이러한 단일 객체 트랜잭션은 동시에 같은 객체에 쓰려고 할 때 갱신 손신을 방지하므로 유용하다.
그러나 일반적인 의미의 트랜잭션은 아니다. 트랜잭션은 보통 다중 객체에 대한 다중 연산을 하나의 실행 단위로 묶는 메커니즘으로 이해된다.

2.2.2. 다중 객체 트랜잭션의 필요성

다중 트랜잭션이 없을 경우 다른 객체에 실행되는 쓰기 작업은 코디네이션 되어야 한다.
다중 객체 트랜잭션은 참조가 유효한 상태로 유지되도록 보장해준다.(외래키 등 참조)
비정규화된 데이터가 동기화가 꺠지는 것을 방지(한번에 여러 문서 갱신 시)
트랜잭션 격리성이 없으면 어떤 색인에서는 레코드가 보이지만 다른 색인은 아직 갱신되지 않아서 레코드가 보이지 않을 수 있다.
이런 기능은 트랜잭션이 없어도 구현이 가능하다.
하지만 원자성이 없으면 오류 처리가 훨씬 더 복잡해지고 격리성이 없으면 동시성 문제가 생길 수 있다.

2.2.3. 오류와 어보트 처리

트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도할 수 있다는 것이다.
그러나 모든 시스템이 이 철학을 따르지 않는다.
ORM 들은 어보트된 트랜잭션을 재시도하지 않는다.
재시도하는 것은 간단하고 효과적인 오류 처리 매커니즘이지만 완벽하지는 않다.
네트워크가 끊겼을 때 재시도하면 트랜잭션이 두 번 실행된다.
오류가 과부하 때문이라면 재시도는 문제를 개선하는 게 아니라 악화시킬 수 있다.
일시적인 오류가 아니라 영구적인 오류면 재시도해도 소용없다.

3. 완화된 격리 수준

두 트랜잭션이 동일한 데이터에 접근하지 않으면 서로 의존하지 않으므로 안전하게 병렬 실행될 수 있다.
동시성 버그는 타이밍에 운이 없을때만 촉발되기 때문에 테스트로 발견이 어렵고 추론도 어렵다.
때문에 DB는 트랜잭션 격리를 제공함으로써 동시성 문제를 감추고자 했다.
격리성은 동시성이 없는 것처럼 행동할 수 있다.
직렬성 격리는 여러 트랜잭션들이 직렬적으로 실행되는 것과 동일한 결과를 보장한다.
허나 현실은 그리 간단하지 않다.
직렬성 격리는 성능 비용 문제로 DB들은 완화된 격리 수준을 사용하는 편이 흔하다.
어떤 동시성 이슈로부터는 보호하지만 모든 이슈로부터는 보호 X

3.1. 커밋 후 읽기(Read Committed)

가장 기본적인 수준의 트랜잭션 격리 수준으로서 두 가지를 보장한다.
데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다. (Dirty Read X)
데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다. (Dirty Write X)

3.1.1. 더티 읽기 방지

Dirty read = 서로다른 트랜잭션에서 커밋되지 않은 데이터를 볼 수 있는 현상
트랜잭션간 갱신된 데이터 읽기는 커밋된 이후에 보여야 한다.
더티 읽기를 막아야 하는 이유
부분적으로 갱신된 상태에서 있는 DB를 보는 것은 사용자에게 혼란스럽고, 다른 트랜잭션들이 잘못된 결정을 하는 원인이 된다.
어보드될 데이터를 볼 수 있기 때문에 혼란스럽다.

3.1.2. 더티 쓰기 방지

Dirty write = 두 트랜잭션이 동일한 객체를 동시에 갱신하려고 할 때, 먼저 쓴 내용이 아직 커밋되지 않은 트랜잭션에서 쓴 것이고 나중에 실행된 쓰기 작업이 커밋되지 않은 값을 덮는 경우
보통 먼전 쓴 트랜잭션이 커밋되거나 어보트될 때까지 두번째 쓰기를 지연시키는 방법을 사용한다.

3.1.3. 커밋 후 읽기 구현

커밋 후 읽기는 매우 널리 쓰이는 격리 수준으로 여러 유명 RDBMS에서 기본 설정으로 사용된다.
더티 읽기 방지
락을 걸어서 객체를 읽기 원하는 트랜잭션이 락을 획득한 후 읽기가 끝나고 해제한다.
그러나, 읽기 락을 하려면 락 대기 경우가 너무 많다.
따라서 커밋 이후에만 데이터를 볼 수 있도록 한다.
데이터베이스는 과거에 커밋된 값과 현재 쓰기 잠금을 갖고 있는 트랜잭션에서 쓴 새로운 값을 기억하고, 커밋되기 전까지는 과거의 값만 보여주도록 한다.
더티 쓰기 방지
가장 흔한 방법으로는 로우 수준 잠금을 사용해 더티 쓰기를 방지한다.
동일한 객체에 쓰기를 원한다면 첫 번째 트랜잭션이 완료된 후에야 락을 얻어 진행할 수 있다.

3.2. 스냅숏 격리와 반복 읽기

앨리스는 일관성이 깨진 상태인 데이터베이스를 본다.
커밋 후 읽기가 모든일을 막아줄 것이라 생각하지만 이 격리 수준에서도 동시성 버그가 생길 수 있는 경우가 아직 많다.
트랜잭션 1이 커밋되고 반영된 결과를 트랜잭션 2의 중간에서 읽어온다면 처음에 읽었던 값과 다른 값이 나올 수 있다.
이런 이상 현상을 비반복 읽기(nonrepeatable read) 혹은 읽기 스큐(read skew)라고 한다.
데이터의 비일관성을 감내할 수 없는 경우가 있다.
백업할 때 일부는 데이터의 과거 버젼, 일부는 새버젼으로 복원하면 비일관성이 영속적으로 된다.
분석 질의를 할 때 불합리한 결과를 볼 수 있다.
스냅숏 격리는 이런 문제의 가장 흔한 해결책이다.
각 트랜잭션은 특정 일관된 스냅숏으로부터만 데이터를 읽도록 만든다.

3.2.1. 스냅숏 격리 구현

다중 버전 객체를 이용한 스냅숏 격리 구현
더티 쓰기를 방지하기 위해 쓰기 락을 사용한다
그러나 읽을 때는 아무 잠금도 필요 없다.
성능 관점에서 스냅숏 격리의 핵심 원리는 읽을 때는 쓰는 쪽을 차단하지 않고, 쓰는 쪽에서 읽는 쪽을 차단하지 않는 것이다.
따라서 락 경쟁 없이 일상적으로 처리되는 것과 동시에 일관성있는 스냅숏에 대해 오래 실행되는 읽기 작업을 동시에 할 수 있다.
데이터베이스는 객체마다 커밋된 버젼 여러개를 유지해야 한다. 이를 다중 버전 동시성 제어(Multi Version Concurrency Control)라고 한다.
커밋 후 읽기 격리에서는 객체마다 두 버젼만 유지하면 된다. 커밋된 버젼과 덮여 쓰여졌지만 아직 커밋되지 않은 버젼.
스냅숏 격리를 지원하는 경우는 거의 질의마다 독립된 스냅숏을 이용하고, 스냅숏 격리는 전체 트랜잭션에 대해 동일한 스냅숏을 사용하도록 한다.

3.2.2. 일관된 스냅숏을 보는 가시성 규칙

동작방식
트랜잭션을 시작할 때 그 시점에 진행 중인 모든 트랜잭션의 목록을 만든다. 이 트랜잭션들이 쓴 데이터는 모두 무시된다.
어보트 된 트랜잭션이 쓴 데이터는 모두 무시된다.
트랜잭션 ID가 더 큰 트랜잭션은 무시한다.
두 조건이 모두 참이면 객체를 볼 수 있다.
읽기를 실행하는 시점 전에 커밋되었거나, 삭제했더라도 삭제한 트랜잭션이 커밋되지 않았다면 데이터를 조회할 수 있다.
오래 실행되는 경우는 오랫동안 스냅숏을 사용해서 오래된 값을 계속 읽을 수도 있다.
데이터베이스는 갱신할 때 값을 교체하지 않고 값이 바뀔 때마다 새 버전을 생성함으로써 작은 오버헤드만 유발하며 일관된 스냅숏 제공 가능

3.2.3. 색인과 스냅숏 격리

다중 버전 데이터베이스에서 색인 질의는 객체의 모든 버젼을 가리키게 하고, 현재 트랜잭션에서 볼 수 없는 버젼을 걸러내게 하면 된다.
현실에서는 여러 구현 세부 사항에 따라 다중 버전 동시성 제어의 성능이 결정된다.
카우치DB, 데이토믹 등 = 전용 B트리를 사용하면 쓰기를 실행하는 모든 트랜잭션은 새로운 B트리 루트를 생성해 일관된 스냅숏으로 사용한다.

3.2.4. 반복 읽기와 혼란스러운 이름

스냅숏 격리는 읽기 전용 트랜잭션에서 유용하다.
여러 RDBMS에서는 각자 다른 이름을 사용한다.
유감스럽게도 SQL 표준의 격리 수준 정의에는 결함이 잇어 모호하고 부적확하며 표준이 그래야하는 것만큼 구현 독립적이 않다.
결과적으로 반복 읽기가 무슨 뜻인지 실제로 아는 사람은 아무도 없다.

3.3. 갱신 손실 방지

두 트랜잭션이 동시에 쓰기를 실행할 때의 문제는 거의 무시했다.
동시에 실행되는 쓰기 트랜잭션 사이에 발생할 수 있는 흥미로운 종류의 충돌이 몇 가지 더 있다.
대표적으로 갱신 손실 문제다.
갱신 손실 문제는 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 발생할 수 있다.

3.3.1. 원자적 쓰기 연산

UPDATE counters SET value = value + 1 WHERE key = 'foo';
SQL
복사
일반적으로 위와같은 연산은 Concurrency safe하다.
원자적 연산은 보통 객체를 읽을 때 Exclusive Lock 을 획득해서 구현한다.
그래서 다른 트랜잭션이 읽지 못해 Cursor Stability 라고 부르기도 한다.
무조건 원자적 연산을 단일 스레드에서 실행하도록 하는 것이다.
ORM을 사용할 경우 R-M-W 주기를 실행하는 코드를 작성하기 쉬워 잠재적으로 테스트로 발견하기 어려운 버그의 원인이 될 수 있다.

3.3.2. 명시적인 잠금

SELECT * FROM figures WHERE name = 'robot' FOR UPDATE;
SQL
복사
애플리케이션에서 갱신할 객체를 명시적으로 잠근다.
이 방법은 동작하지만 올바르게 동작하려면 애플리케이션 로직에 대해 신중하게 생각해야 한다.
코드의 어딘가에 필요한 잠금을 추가하는 것을 잊어버려 경쟁조건을 유발하기 쉽다.

3.3.3. 갱신 손실 자동 감지

원자적 연산과 잠금은 R-M-W 이 순차적으로 실행되도록 강제한다.
대안으로 병렬 실행을 허용하고, 손실을 발견하면 트랜잭션을 어보트시키고 재시도하도록 강제한다.
이 방법의 이점은 스냅숏 격리와 결합해 효율적으로 수행할 수 있다.
실제로 Repeatable Read, 스냅숏 격리 수준들은 갱신 손실이 발생하면 자동으로 발견해서 문제가 되는 트랜잭션을 어보트시킨다.
갱신 손실 감지는 애플리케이션 코드에서 어떤 특별한 데이터베이스 기능도 쓸 필요가 없게 도와주므로 매우 좋은 기능이다.
원자적 연산을 쓰는걸 까먹을 경우 버그를 유발하지만 자동으로 갱신 손실이 감지되 오류가 덜 발생한다.

3.3.4. Compare-and-set

UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content'
SQL
복사
트랜잭션을 제공하지 않는 DB 중에는 Compare-and-set 연산을 제공하는 것도 있다.
값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용한다.
현재 값이 이전에 읽은 값과 일치하지 않으면 갱신을 반영하지 않고 재시도해야 한다.
그러나 데이터베이스가 스냅숏으로 읽고 있으면 갱신 손실을 막지 못한다.

3.3.5. 충돌 해소와 복제

복제가 적용된 DB에서는 여러 노드에 데이터의 복사본이 있어서 데이터가 다른 노드들에서 동시에 변경될 수 있다.
따라서 갱신 손실을 방지하려면 추가 단계가 필요하다.
잠금과 compare-and-set은 복제된 상태에서 동시에 여러 쓰기가 실행되고 복제되는 것을 허용하므로 최신 복사본이 하나만 있으라 보장할 수 없다.
그래서 불가능하다!
대신 동시에 쓰기가 실행될 때 여러 개의 충돌된 버전을 생성하고 사후에 애플리케이션 코드에서 충돌을 해소하고 병합하는 것이 흔히 쓰는 방법
반면 최종 쓰기 승리(LWW) 방법은 갱신 손실이 많이 발생하지만, 이게 기본 설정인 복제 데이터베이스가 많다.

3.4. 쓰기 스큐와 팬텀

어플리케이션 버그를 유발하는 쓰기 스큐의 예
데이터의 오염을 피하려면 경쟁 조건을 방지해야 한다.
그러나 동시성으로 발생하는 경쟁 조건은 이게 전부가 아니다.
앞의 사진에서 두 트랜잭션 모두 커밋되고 호출 대기 의사는 한명도 없게 된다.
최소 한 명의 의사가 호출 대기해야 한다는 요구사항을 위반한다.

3.4.1. 쓰기 스큐를 특징 짓기

이런 현상을 쓰기 스큐라고 한다.
두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티 쓰기도 갱신 손실도 아니다.
쓰기 스큐를 갱신 손실 문제가 일반화된 것으로 생각할 수도 있다.
쓰기 스큐는 두 트랜잭션이 같은 객체들을 읽어서 그중 일부를 갱신할 때 나타날 수 있다.
다른 객체들이므로 원자적 단일 객체 연산은 도움이 되지 않는다.
갱신 손실 자동감지도 도움이 되지 않는다. 스냅숏 격리 수준에서 감지를 할 수 없는 소프트웨어적 요구사항이기 때문이다.
데이터베이스 내에서 제약 조건을 설정할 수 있는 건 외래 키 제약, 특정 값 제한 등이다. 특정 조건의 레코드의 총 수 같은 것들은 구체화 뷰를 사용해 구현은 가능하다.

3.4.2. 쓰기 스큐를 유발하는 팬텀

쓰기 스큐를 유발하는 패턴
1.
특정 요구사항을 만족하는지 확인한다
2.
1의 질의 결과에 따라애플리케이션 코드를 어떻게 진행할지 결정한다.
3.
처리하기로 결정됐따면 데이터베이스에 쓰기 트랜잭션을 커밋한다.
a.
이 커밋에 따라 1.이 바뀐다. 즉, 1의 조건에 부합하는 결과가 변경된다 (의사가 한 명 줄었다. 해당 시간에 예약이 되었다 등등)
의사 호출의 예시에서는 1단계의 로우를 잠금으로써 쓰기 스큐를 회피할 수 있지만, 아무 로우도 반환되지 않는 경우는 아무것도 잠글 수가 없다.
어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과를 팬텀(Phantom)이라고 한다.

3.4.3. 충동 구체화

잠글 수 없어서 팬텀 문제가 생긴다면, 임의적으로 데이터베이스에 잠금 객체를 생성하자.
회의실의 경우는 모든 시간 범위의 조합을 미리 만들어두고 필요할 때 락을 걸 수 있다.
이런 방법을 충돌 구체화라고 한다.
팬텀을 구체적인 로우 집합에 대한 잠금 충돌로 변화하기 때문이다.
그러나 동시성을 해결하기 위해 데이터 모델을 이용하는 것은 좋지 않으므로 최후의 대안이다.
이 방법보다는 직렬성 격리 수준을 선호한다.

4. 직렬성

격리 수준은 이해하기 어렵고 데이터베이스마다 그 구현에 일관성이 없다.
애플리케이션 코드를 보고 특정한 격리 수준에서 해당 코드를 실행하는 게 안전한지 알기 어렵다.
경쟁 조건을 감지하는 데 도움이 되는 좋은 도구가 없다.
⇒ 해결을 위해선 직렬성 격리를 사용하라
직렬성 격리 = 여러 트랜잭션이 병렬로 실행되어도 최종 결과는 한 번에 하나씩 직렬로 실행될 때와 같도록 보장한다.
DB가 발생할 수 있는 모든 경쟁 조건을 막아준다.
직렬성을 구현하는 방법
실제적인 직렬 실행(순차적 트랜잭션)
2단계 잠금 (2PL)
직렬성 스냅숏 격리 같은 낙관적 동시성 제어 기법

4.1. 실제적인 직렬 실행

동시성 문제를 피하는 가장 간단한 방법은 동시성을 완전히 제거하는 것이다.
한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하면 된다
그러나 처리량은 CPU 코어 하나의 처리량으로 제한된다.
그래서 전통적인 방식보다는 다른 구조를 써야 한다.

4.2. 트랜잭션을 스토어드 프로시저 안에 캡슐화 하기

상호작용식 트랜잭션과 스토어드 프로시저의 차이점
데이터베이스 초창기에는 트랜잭션이 사용자의 활동 전체 흐름을 포함할 수 있게 하려는 의도가 있었다.
항공권 예약의 여러 과정 (경로 선택, 요금, 가용 좌석 탐색, 여행 일정표 정하기, …) 을 하나의 트랜잭션으로 표현하고 원자적으로 커밋될 수 있다면 깔끔할 것으로 생각했다
OLTP 애플리케이션은 트랜잭션을 짧게 유지하기 위해 새로운 HTTP 요청은 새로운 트랜잭션을 시작한다.
상호작용식 트랜잭션은 애플리케이션과 DB사이의 네트워크 통신에 많은 시간을 소비한다.
때문에 단일 스레드에서는 상호작용하는 다중 구문 트랜잭션을 허용하지 않는다.
대신, 이 트랜잭션 코드 전체를 스토어드 프로시저 형태로 미리 데이터베이스에 제출한다.
따라서 네트워크나 디스크 I/O 대기 없이 매우 빨리 실행된다.

4.2.1. 스토어드 프로시저의 장단점

단점
데이터베이스 벤더마다 제각각의 스토어드 프로시저용 언어가 발목을 잡는다.
발전도 느리고 라이브러리 생태계가 빈약하다.
DB에서 실행되는 코드는 관리/디버깅/배포/테스트 어렵다.
데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감하다.
극복
현대의 스토어드 프로시저용 언어 버리고 범용 프로그래밍 언어를 사용한다.
스토어드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에서 실행할 정도로 처리량이 나온다.
I/O 대기 필요 없고, 동시성 제어 메커니즘에서 발생하는 오버헤드를 피한다.

4.3. 파티셔닝

모든 트랜잭션을 순차적으로 실행하면 간단해지지만 트랜잭션 처리량이 단일 장비의 단일 CPU 코어의 속도로 제한된다.
읽기 전용 트랜잭션은 스냅숏 격리로 실행될 수 있지만 쓰기 처리량이 높으면 심각한 병목이 된다.
여러 CPU 코어와 여러 노드로 확장하기 위해 파티션 할 수도 있다.
각 트랜잭션이 단일 파티션 내에서만 데이터를 읽고 쓰도록 하고, 다른 파티션과 독립적으로 실행되는 트랜잭션 처리 스레드를 가지도록 한다.
그러나, 여러 파티션에 접근해야 하는 트랜잭션이 있다면 접근하는 모든 파티션에 걸쳐서 코디네이션을 해야 한다.
여러 파티션에 걸친 트랜잭션은 추가적인 코디네이션 오버헤드가 있으므로 단일 파티션 트랜잭션보다 엄청나게 느리다.
따라서 트랜잭션이 단일 파티션에서 실행되어야만 가능하고, 보조 색인이 여러개면 또 여러 파티션에 걸쳐서 코디네이션이 필요할 수 있다.

4.3.1. 직렬 실행 요약

몇 가지 제약 사항 안에서 직렬성 격리를 획득하는 실용적인 방법이다.
모든 트랜잭션이 작고 빨라야 한다.
활성화된 데이터 셋이 메모리에 적제될 수 있어야 한다.
쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 낮아야 한다.
여러 파티션에 걸친 트랜잭션을 쓸 수는 있지만 제한이 많다.

4.4. 2단계 잠금(2PL)

2잠금은 지난 30년간 데이터베이스에서 직렬성을 구현하는데 널리 쓰인 유일한 알고리즘이다.
더티 쓰기를 막는 데 잠금을 자주 사용하는데 두 개의 트랜잭션이 동시에 같은 객체를 쓰려고 하면 잠금은 나중에 쓰는 쪽이 진행하기 전에 먼저 쓰는 쪽에서 트랜잭션을 완료 할 때까지 기다리도록 보장해준다.
2단계 잠금은 쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있다.
그러나 누군가 어떤 객체에 쓰려고 하면 독점적인 접근이 필요하다.
트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰기를 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다(이렇게 하면 B가 A 몰래 갑자기 객체를 변경하지 못하도록 보장된다).
트랜잭션 A가 객체에 썼고 트랜잭션 B가 그 객체를 읽기 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다
2PL은 직렬성을 제공하므로 스냅숏 격리에서 발생할 수 있는 갱신 손실이나 쓰기 스큐를 포함한 모든 경쟁 조건으로부터 보호해준다.

4.4.1. 2단계 잠금 구현

2PL은 MySQL 에서 직렬성 격리 수준을 구현하는데 사용되고 DB2에서는 반복 읽기로 사용된다.
읽는 쪽과 쓰는 쪽을 막는 것은 각 객체에 잠금을 사용해 구현한다.
잠금은 공유 모드(Shared Lock)나 독점 모드(Exclusive Lock)로 사용될 수 있다.
잠금이 아주 많이 사용되므로 교착 상태가 매우 쉽게 발생할 수 있다.

4.4.2. 2단계 잠금의 성능

2단계 잠금의 가장 큰 약점이 성능이다.
원인은 잠금을 획득하고 해제하는 오버헤드 때문이지만 더 중요한 원인은 동시성이 줄어드는 것이다.

4.4.3. 서술 잠금

직렬성 격리를 쓰는 데이터베이스는 팬텀을 막아야한다.
회의실 예시로 설명하면 한 트랜잭션이 특정 시간 범위의 회의실을 검색했다면 다른 트랜잭션이 동일한 회의실에 삽입하거나 갱신할 수 없다.
이런 경우 서술 잠금(predicate lock)이 필요하다.
공유/독점 잠금과 유사하지만, 서술 잠금은 특정 객체에 속하지 않고 검색 조건에 부합하는 모든 객체에 속한다.
MySQL의 갭락을 의미하는거 같다.

4.4.4. 색인 범위 잠금

그러나 서술 잠금은 잘 동작하지 않는다.
다른 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 락을 얻기에 오래 걸린다
따라서 대부분 데이터는 실제로는 색인 범위 잠금(index-range, next-key locking)을 구현한다.
더 많은 객체가 부합하도록 서술 조건을 간략화 하는 것은 안전하다.
정오와 오후 1시 사이에 123번 방을 예약하는 것에 대한 서술 잠금을 → 모든 시간 범위에 123번 방을 예약하는 것으로 근사시켜 잠금 실행
이 방법을 쓰면 서술 잠금보다 정밀하지 않지만, 오버헤드가 낮아서 좋은 타협안이 된다.
만약 적합한 색인이 없으면 테이블 전체를 공유 잠금할 수 있다. 물론 성능은 좋지 않다.

4.5. 직렬성 스냅숏 격리(SSI)

동시성 제어를 하는 방법
1.
성능이 좋지만 다양한 경쟁 조건(갱신 손실, 쓰기 스큐, 팬텀 등)에 취약한 완화된 격리 수준(커밋 후 읽기 스냅숏 격리)
2.
성능이 좋지 않거나(2PL) 확장이 잘 되지 않는 직렬성 구현(직렬 실행)
직렬성 격리와 좋은 성능을 공존하려면 어떻게 해야 할까? 직렬성 스냅숏 격리(Serializable snapshot isolation) 을 사용하자.
SSI는 단일 노드 데이터베이스와 분산 데이터베이스 모두에서 사용된다.

4.5.1. 비관적 동시성 제어 대 낙관적 동시성 제어

2단계 잠금은 비관적 동시성 제어 메커니즘이다.
뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때 까지 기다리는게 낫다는 원칙
직렬성 스냅숏 격리는 낙관적 동시성 제어 메커니즘임
우선 동시에 진행하고, 커밋되기를 원할 때 격리가 위반되었다면 어보트하고 재시도한다.
낙관적 동시성 제어 방법의 경우, 트랜잭션의 동일 객체 접근이 너무 많으면 어보트할 트랜잭션의 비율이 높아지므로 성능이 떨어진다.