개발독서/데이터

[고투런1기] 데이터 중심 애플리케이션 설계 (7장 트랜잭션)

보리시스템 2024. 6. 24.

[2부 분산 데이터]

07장: 트랜잭션
1. 애매모호한 트랜잭션의 개념

1.1 ACID의 의미
1.2 단일 객체 연산과 다중 객체 연산

2. 완화된 격리 수준
2.1 커밋 후 읽기
2.2 스냅숏 격리와 반복 읽기
2.3 갱신 손실 방지
2.4 쓰기 스큐와 팬텀

3. 직렬성
3.1 실제적인 직렬 실행
3.2 2단계 잠금(2PL)
3.3 직렬성 스냅숏 격리(SSI)

[정리: 여러 장비로 분산해 저장하는 데이터]

- 트랜잭션

=> 애플리케이션이 어떤 동시성 문제와 어떤 종류의 하드웨어와 소프트웨어 결함이 존재하지 않는 것처럼 동작할 수 있게 도와주는 추상층
=> 트랜잭션이 없으면 다양한 방법으로 데이터가 일관성이 깨질 수 있음

- 동시성 제어 - 경쟁 조건
1) 더티읽기: 한 클라이언트가 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 읽음. 커밋 후. 읽기 또는 그보다 강한 격리 수준은 더티읽기를 방지
2) 더티쓰기: 한 클라이언트가 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 덮어씀. 거의 모든 트랜잭션 구현은 더티쓰기를 방지
3) 읽기스큐(비반복읽기): 클라이언트는 다른 시점에 데이터베이스의 다른 부분을 봄. 이 문제를 막기 위한 해결책으로 트랜잭션이 어느 시점의 일관된 스냅숏으로부터 읽는 스냅숏 격리(다중버전동시성제어(MVC)를 써서 구현)를 가장 흔히 사용
4) 갱신손실: 두 클라이언트가 동시에 read-modify-wite 주기를 실행. 한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 채로 다른트랜잭션이 쓴 내용을 덮어써서 데이터가 손실됨. 스냅숏 격리 구현 중 어떤 것은 이런 이상 현상을 자동으로 막아주지만 그렇지 않은 것은 수동잠금(SELECT FOR UPDATE)이 필요
5) 쓰기 스큐: 트랜잭션이 무언가를 읽고 읽은 값을 기반으로 어떤 결정을 하고 그 결정을 데이터베이스에 씀. 그러나 쓰기를 실행하는 시 점에는 결정의 전제가 더이상 참이 아님. 직렬성 격리만 이런 이상 현상을 막을 수 있음
6) 팬덤 읽기: 트랜잭션이 어떤 검색 조건에 부합하는 객체를 읽음. 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 실행. 스냅숏 격리는 간단한 팬텀 읽기는 막아주지만 쓰기 스큐 맥락에서 발생하는 팬텀은 색인범위잠금처럼 특별한 처리가 필요

- 직렬성 트랜잭션 구현 방법
1) 트랜잭션 순서대로 실행하기: 트랜잭션의 실행 시간이 아주 짧고 트랜잭션 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 트랜잭션 처리량이 낮다면 아주 간단하고 효과적인 선택
2) 2단계 잠금: 수십년 동안 직렬성을 구현하는표준적인 방법이었지만 성능 특성 때문에 사용을 피하는 애플리케이션이 많아짐
3) 직렬성 스냅숏 격리(SSI): 낙관적 방법을 사용해 트랜잭션이 차단되지 않고 진행할 수 있게 함. 트랜잭션이 커밋을 원할 때 트랜잭션을 확인해서 실행이 직렬적이지 않다면 어보트시킴

 


 

7장 트랜잭션

- 트랜잭션?
=> 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법
=> 한 트랜잭션 내의 모든 읽기와 쓰기는 한. 연산으로 실행
=> 트랜젝션을 쓰면 애플리케이션에서 오류 처리를 하기가 훨씬 단순해짐(부분적인 실패를 걱정할 필요가 없기 때문
=> 프로그래밍 모델을 단순화하려는 목적으로 만든 것
=> 안전성 보장(잠재적인 오류 시나리오나 동시성 문제를 해결)

 


 

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

- 2000년대 후반 비관계형 DB의 인기와 더불어 복제, 파티셔닝 기능의 제공에 따라 새로운 세대의 DB 중 다수는 트랜잭션을 완전히 포기하거나 이전보다 훨씬 약한 보장의 기능을 함
=> 대규모 시스템이라면 높은 성능과 고가용성 유지를 위해 트랜잭션 포기가 필요하다는 믿음이 널리 퍼짐
=> 반면 DB 벤더에서는 트랜잭션적인 보장이 필수적인 요구사항이라고 주장하기도 했음
=> 이처럼 트랜잭션은 이점과 한계가 있음

 


 

1.1 ACID의 의미

- 트랜잭션이 제공하는 안전성 보장 'ACID'
=> 원자성(Acomicity), 일관성(Consistency). 격리성(Isolation), 지속성(Durability)
=> 데이터베이스에서 내결함성 메커니즘을 나타내는 정확한 용어를 확립하기 위해 ACID 용어를 만듦
=> 그러나 현실에서는 데이터베이스마다 ACID 구현이 제각각

- 원자성
=> 여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여 있는데 결함 때문에 완료(커밋)될 수 없다면 어보트되고 데이터베이스는이 트랜잭션에서 지금까지 실행한 쓰기를 무시하거나 취소해야 함
=> 오류가 생겼을 때 트랜잭션을 어보트하고 해당 트랜잭션에서 기록한 모든 내용을 취소하는 능력

- 일관성
=> 데이터에 관한 어떤 선언, 불변식(invarian)이 있다는 것
=> 원자성, 격리성, 지속성은 데이터베이스의 속성인 반면 일관성은 애플리케이션의 속성

- 격리성
=> 동시에 실행되는 트랜잭션은 서로 격리된다는 것
=> 고전적인 데이터베이스 교과서에서는 격리성을 직렬성이라는 용어로 공식화함
* 직렬성은 각 트랜잭션이 전체 데이터베이스에서 실행되는 유일한 트랜잭션인 것처럼 동작할 수 있다는 것
* 그러나 직렬성 격리(serializable isolation)는 성능 손해를 동반하므로 현실에서는 거의 사용되지 않음

- 지속성
=> 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장
=> 지속성을 보장하려면 데이터베이스는 트랜젝션이 성공적으로 커밋됐다고 보고하기 전에 쓰기나 복제가 완료될 때까지 기다려야 함

 



1.2 단일 객체 연산과 다중 객체 연산

- 다중 객체 트랜잭션
=> 흔히 데이터의 여러 조각이 동기화된 상태로 유지돼야할 때 필요
=> 어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야 함
=> 관계형 데이터베이스에서 이것은 전형적으로 클라이언트와 데이터베이스 서버 사이의 TCP 연결을 기반으로 함
* 비관계형 데이터베이스는 이런식으로 연산을 묶는 방법이 없는 경우가 많음

- 단일 객체 쓰기
=> 여러 클라이언트에서 동시에 같은 객체에 쓰려고 할 때 갱신손실(lost update)을 방지하므로 유용함
=> 그러나 일반적으로 쓰이는 의미의 트랜잭션이 아님

- 오류와 어보트 처리
=> 트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도할 수 있다는 것
=> 어보트된 트랜잭션을 재시도하는 것은 간단하고 효과적인 오류처리 메커니즘이지만 완벽하지는 않음

 



2. 완화된 격리 수준

- 동시성 문제(경쟁조건)는 트랜잭션이 다른 트랜잭션에서 동시에 변경한 데이터를 읽거나 두 트랜잭션이 동시에 같은 데이터를 변경하려고 할 때만 나타남
=> 데이터베이스는 오랫동안 트랜잭션 격리를 제공함으로써 애플리케이션 개발자들에게 동시성 문제를 감추려고 했음
=> 직렬성 격리는 데이터베이스가 여러 트랜잭션들이 직렬적으로 실행되는 것(즉 동시성 없이 한번에 트랜잭션 하나만 실행)과 동일한 결과가 나오도록 보장한다는 것을 의미

 


 

2.1 커밋 후 읽기(read committed)

- 보장해주는 2가지
1) 데이터베이스에서 읽을 때 커밋된 데이터만 보게 됨 (더티읽기가 없음)
2) 데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 됨 (더티쓰기가 없음)

- 더티 읽기 방지
=> 커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 읽기를 막아야 함
=> 트랜잭션이 쓴 내용이 커밋된 후에야 다른 트랜잭션에게 보인다는 의미

- 더티 쓰기 방지
=> 커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티쓰기를 방지해야 함
=> 보통 먼저 쓴 트랜잭션이 커밋되거나 어보트될 때까지 두번째 쓰기를 지연시키는 방법을 사용

- 커밋 후 읽기 구현
=> 커밋 후 읽기는 매우 널리 쓰이는 격리수준으로 오라클11g, 포스트그레스큐엘, SOL 서버 2012, 멤SQL(MemSQL) 등에서는 기본 설정임
=> 가장 흔한 방법으로 데이터베이스는 로우 수준 잠금을 사용해 더티쓰기를 방지
=> 더티읽기를 막을 수 있는 방법으로는 동일한 잠금을 써서 객체를 읽기 원하는 트랜잭션이 잠시 잠금을 획득한 후 읽기가 끝난 후바로 해제하게 하는 것
* 이렇게 하면 객체가 변경됐으나 아직 커밋되지 않은 값을 갖고 있을 때 읽기가 실행되지 않도록 보장할 수 있음
=> 그러나 읽기 잠금을 요구하는 방법은 현실에서는 잘 동작하지 않음. 읽기만 실행하는 여러 트랜잭션들이 오랫동안 실행되는 쓰기 트랜잭션 하나가 완료될 때까지 기다려야 할 수 있기 때문

- 스냅숏 격리와 반복 읽기
=> 커밋 후 읽기 격리를 피상적으로 보면 트랜잭션이 해야 하는 모든 일을 해주는 것으로 생각하는 것도 무리가 아님
=> 어보트를 허용하고(원자성에 필요) 트랜잭션의 미완료된 결과를 읽는 것을 방지하며 동시에 실행되는 쓰기가 섞이는 것을 막아줌
=> '비반복 읽기(non repeatable read)', '읽기 스큐(read skew)'의 이상 현상에 대한 해결책으로는 스냅숏 격리가 있음
=> 각 트랜잭션은 데이터베이스의 일관된 스냅으로부터 읽음. 즉 트랜잭션은 시작할 때 데이터베이스에 커밋된 상태였던 모든 데이터를 봄. 데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 볼 뿐임
=> 스냅숏 격리는 백업이나 분석처럼 실행하는데 오래 걸리며 읽기만 실행하는 질의에 요긴

- 스냅숏 격리 구현
=> 스냅숏 격리 구현은 커밋 후 읽기 격리처럼 전형적으로 더티쓰기를 방지하기 위해 쓰기 잠금을 사용
=> 쓰기를 실행하는 트랜잭션은 같은 객체에 쓰는 다른 트랜잭션의 진행을 차단할 수 있음
=> 그러나 읽을 때는 아무 잠금도 필요없음. 성능 관점에서 스냅숏 격리의 핵심 원리는 읽는쪽에서 쓰는쪽을 결코 차단하지 않고 쓰는쪽에서 읽는쪽을 결코 차단하지 않는다는 것
=> 따라서 데이터베이스는 잠금경쟁 없이 쓰기작업이 일상적으로 처리되는 것과 동시에 일관성 있는 스냅숏에 대해 오래 실행되는 읽기작업을 처리할 수 있음

- 다중 버전 동시성 제어(multi-version concurrency control, MVCC)
=> 데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 함. 진행 중인 여러 트랜잭션에서 서로 다른 시점의 데이터베이스 상태를 봐야 할 수도 있기 때문

- 일관성 스냅숏을 보는 가시성 규칙
=> 트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정. 면밀하게 가시성 규칙을 정의함으로써 데이터베이스는 데이터베이스의 일관된 스냅숏을 애플리케이션에게 제공할 수 있음
=> 동작 방식
1) 데이터베이스는 각 트랜잭션을 시작할 때 그 시점에 진행 중인(아직 커밋이나 어보트가 되지 않은) 모든 트랜잭션의 목록을 만듦(이 트랜잭션들이 쓴 데이터는 모두 무시됨)
2) 어보트된 트랜잭션이 쓴 데이터는 모두 무시됨
3) 트랜잭션ID가 더 큰(즉 현재 트랜잭션이 시작한 후에 시작한) 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계없이 모두무시됨
4) 그밖의 모든 데이터는 애플리케이션의 질의로 볼 수 있음

- 두 조건이 모두 참이면 객체를 볼 수 있음
1) 읽기를 실행하는 트랜잭션이 시작한 시점에 읽기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태였음
2) 읽기 대상 객체가 삭제된 것으로 표시되지 않았음. 또는 삭제된 것으로 표시됐지만 읽기를 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 아직 커밋되지 않았음
=> 오래 실행되는 트랜잭션은 오랫동안 스냅숏을 사용해서(다른 트랜잭션의 관점에서) 덮어써지거나 삭제된지 오래된 값을 계속 읽을 수도 있음
=> 데이터베이스는 갱신할 때 값을 교체하지 않고 값이 바뀔때마다 새 버전을 생성함으로써 작은 오버헤드만 유발하면서 일관된 스냅숏을 제공할 수 있음

- 색인과 스냅숏 격리
=> 다중 버전 데이터베이스에서 색인 동작 방법은 단순하게 색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내게 하는 것임
=> 가비지 컬렉션이 어떤 트랜잭션에게도 더 이상 보이지 않는 오래된 객체 버전을 삭제할 때. 대응되는 색인 항목도 삭제됨


- 반복 읽기와 혼란스러운 이름
=> 스냅숏 격리는 유용한 격리 수준이며 특히 읽기 전용 트랜잭션에 유용함
=> 데이터베이스마다 다른 이름을 사용하기도 해 혼란스러울 수 있는데 이는 SQL 표준에 스냅숏 격리의 개념이 없기 때문
* 오라클: 직렬성, 포스트그레스큐엘/마이SQL: 반복 읽기

 



2.3 갱신 손실 방지

- 갱신손실(lost update) 문제
=> 동시에 실행되는 쓰기 트랜잭션 사이에 발생할 수 있는 충돌
=> 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때(read-modify-write 주기) 발생할 수 있음
=> 흔한 문제이기 때문에 다양한 해결책이 개발됨

- 원자적 쓰기 연산
=> 여러 데이터베이스에서 원자적 갱신 연산을 제공
=> 이 연산은 애플리케이션 코드에서 read-modify-write 주기를 구현할 필요를 없애줌
=> 원자적 연산은 보통 객체를 읽을 때 그 객체에 독점적인(exclusive) 잠금을 획득해서 구현함
=> 그래서 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못하게 함(이 기법을 커서 안정성, cursorstability이라고 부르기도 함

- 명시적인 잠금
=> 애플리케이션에서 갱신할 객체를 명시적으로 잠금으로써 애플리케이션이 read-modify-write 주기를 수행할 수 있고 다른 트랜잭션이 동시에 같은 객체를 읽으려고 하면 첫번째 read-modify-write 주기가 완료될 때까지 기다리도록 강제됨
=> 이 방법은 올바르게 동작하게 하려면 애플리케이션 로직에 대해 신중하게 생각해야 함. 코드의 어딘가에 필요한 잠금을 추가하는 것을 잊어버려서 경쟁조건을 유발하기 쉬움

- 갱신 손실 자동 감지
=> 원자적 연산과 잠금은 read-modify-write 주기가 순차적으로 실행되도록 강제함으로써 갱신손실을 방지
=> 대안으로 이들의 병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트시키고 read-modity-write 주기를 재시도 하도록 강제하는 방법이 있음

- Compare-and-set
=> 트랜잭션을 제공하지 않는 데이터베이스 중에는 원자적 compare-and-set 연산을 제공하는 것도 있음
=> 이 연산의 목적은 값을 마지막으로 읽은 후로 변경되지 않았을때만 갱신을 허용함으로써 갱신손실을 회피하는 것
=> 현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지 않고 read-modify-wrie 주기를 재시도해야 함

- 충돌 해소와 복제
=> 복제가 적용된 데이터베이스에서 흔히 쓰는 방법은 쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전(형제, sibling)을 생성하는 것을 허용하고 사후에 애플리케이션 코드나 특별한 데이터 구조를 사용해 충돌을 해소하고 이 버전들을 병합하는 것임
=> 원자적 연산은 복제 상황에서도 잘 동작함. 특히 교환법칙이 성립하는 연산인 경우 더욱 그러함(즉 다른 복제본에 다른 순서로 연산을 적용해도 같은 결과가 나오는 경우)
=> 반면 최종 쓰기 승리(last write wins, LWW) 충돌 해소 방법 은 갱신 손실이 발생하기 쉬움. 하지만 많은 복제 데이터베이스는 LWW가 기본 설정임

 



2.4 쓰기 스큐와 팬텀

- 쓰기 스큐
=> 두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티쓰기도 갱신손실도 아닌 경쟁조건임
=> 쓰기 스큐를 갱신 손실 문제가 일반화된 것으로 생각할 수도 있음. 쓰기 스큐는 두. 트랜잭션이 같은 객체들을 읽어서 그중 일부를 갱신할 때 나타날 수 있음
=> 다른 트랜잭션이 하나의 동일한 객체를 갱신하는 특별한 경우에 더티쓰기나 갱신손실 이상현상을 겪게 됨

- 쓰기 스큐를 유발하는 팬텀
=> 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과
=> 읽기 쓰기 트랜잭션에서는 팬텀이 쓰기 스큐의 특히 까다로운 경우를 유발할 수 있음

- 충돌 구체화
=> 팬텀을 데이터베이스에 존재하는 구체적인 로우 집합에 대한 잠금 충돌로 변환
=> 충돌을 구체화하는 방법은 알아내기 어렵고 오류가 발생하기 쉬움
=> 따라서 충돌 구체화는 다른 대안이 불가능할 때 최후의 수단으로 고려해야 함

 



3. 직렬성

- 직렬성 격리
=> 보통 가장 강력한 격리 수준이라고 여겨짐
=> 여러 트랜잭션이 병렬로 실행되더라도 최종결과는 동시성 없이 한번에 하나씩 직렬로 실행될 때와 같도록 보장
=> 따라서 데이터베이스는 트랜잭션을 개별적으로 실행할 때 올바르게 동작한다면 이들을 동시에 실행할 때도 올바르게 동작할 것을 보장해줌 (즉 데이터베이스가 발생할 수 있는 모든 경쟁조건을 막아줌)

 


 

3.1 실제적인 직렬 실행

- 동시성 문제를 피하는 가장 간단한 방법
=> 동시성을 완전히 제거하는 것
=> 한번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하면 됨. 그러면 트랜잭션 사이의 충돌을 감지하고 방지하는 문제를 완전히 회피할 수 있음. 결과적으로 격리수준은 직렬성 격리가 됨

- 단일 스레드 실행이 가능하게 된 이유
1) 램 가격이 저렴해져서 많은 사용 사례에서 활성화된 데이터셋 전체를 메모리에 유지할 수 있을 정도가 됨
=> 트랜잭션이 접근해야 하는 모든 데이터가 메모리에 있다면 데이터를 디스크에서 읽어오기를 기다려야 할 때보다 트랜잭션이 훨씬 빨리 실행될 수 있음
2) 데이터베이스 설계자들은 OLTP 트랜잭션이 보통 짧고 실행하는 읽기와 쓰기의 개수가 적다는 것을 깨달음. 반대로 오래 실행되는 분석질의는 전형적으로 읽기전용이라서 직렬실행루프 밖에서(스냅숏 격리를 사용해) 일관된 스냅숏을 사용해 실행할 수 있음

- 트랜잭션을 스토어드 프로시저 안에 캡슐화하기
=> 데이터베이스의 초창기에는 데이터베이스 트랜잭션이 사용자 활동의 전체 흐름을 포함할 수 있게 하려는 의도가 있었음
=> 트랜잭션은 계속 상호작용하는 클라이언트/서버 스타일로 실행돼 왔음 (한번에 구문 하나씩 실행하는 방식)
=> 이러한 상호작용 시 트랜잭션은 애플리케이션과 데이터베이스 사이의 네트워크 통신에 많은 시간을 소비함
=> 이런 이유로 단일스레드에서 트랜잭션을 순차적으로 처리하는 시스템들은 상호작용하는 다중구문 트랜잭션을 허용하지 않음. 대신 애플리케이션은 트랜잭션 코드 전체를 스토어드 프로시저 형태로 데이터베이스에 미리 제출해야 함

- 스토어드 프로시저
=> 단점
1) 데이터베이스 벤더마다 제각각 스토어드 프로시저용 언어가 있고 이 언어들은 범용 프로그래밍 언어의 발전을 따라잡지 못해 오늘날의 관점에서 매우 조잡하고 낡아 보이며 대부분의 프로그래밍 언어에서 찾을 수 있는 라이브러리 생태계가 빈약
2) 데이터베이스에서 실행되는 코드는 관리하기 어려움. 애플리케이션 서버와 비교할때 디버깅하기 어렵고 버전 관리 및 배포가 불편하여 테스트하기도 까다롭고 모니터링용 지표 수집 시스템과 통합하기도 어려움
3) 데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감할 때가 많음. 흔히 여러 애플리케이션 서버에서 데이터베이스 인스턴스 하나를 공유하기 때문

=> 현대의 스토이드 프로시저 구현은 PL/SQL을 버리고 대신 기존의 범용 프로그래밍 언어를 사용해 이런 문제를 극복
* 스토이드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일스레드에서 실행하는 것이 현실성 있음
* I/O 대기가 필요 없고 다른 동시성 제어 메커니즘의 오버헤드를 회피하므로 단일 스레드로 상당히 좋은 처리량을 얻을 수 있음

- 파티셔닝
=> 여러 CPU 코어와 여러 노드로 확장하기 위해 데이터를 파티셔닝할 수 있음
=> 단순한 키-값 데이터는 쉽게 파티셔닝될 수 있지만 여러 보조 색인이 있는 데이터는 여러 파티션에 걸친 코디네이션이 많이 필요할 가능성이 높음

- 직렬 실행 요약
=> 트랜잭션 직렬 실행은 몇가지 제약사항 안에서 직렬성 격리를 획득하는 실용적인 방법이 됨
=> 모든 트랜잭션은 작고 빨라야 함. 느린 트랜잭션 하나가 모든 트랜잭션 처리를 지연시킬 수 있기 때문
=> 활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한됨. 거의 접근되지 않는 데이터는 잠재적으로 디스크로 옮겨질 수 있지만 단일 스레드 트랜잭션에서 이에 접근해야 한다면 시스템이 매우 느려짐
=> 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 함. 그렇지 않으면 여러 파티션에 걸친 코디네이션이 필요하지 않도록 트랜잭션을 파티셔닝 해야 함
=> 여러 파티션에 걸친 트랜잭션도 쓸 수 있지만 이것을 사용할 수 있는 정도에는 엄격한 제한이 있음

 



3.2 2단계 잠금(2PL)

- 2단계 잠금
=> 직렬성을 구현하는 데 널리 쓰인 유일한 알고리즘
=> 2PL에서 쓰기 트랜잭션은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막고 그 역도 성립
=> 스냅숏 격리는 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않는다는 원칙이 있는데 이게 2PL와의 중요한 차이
=> 2PL은 직렬성을 제공하므로 갱신손실과 쓰기스큐를 포함한 모든 경쟁조건으로부터 보호해줌

- 2PL 구현
=> 읽는쪽과 쓰는쪽을 막는 것은 데이터베이스의 각 객체에 잠금을 사용해 구현함. 잠금은 공유모드(sharedmode)나 독점모드(exclusivemode)로 사용될 수 있음

- 2PL 성능
=> 2PL의 큰 약점은 성능으로 2PL을 쓰면 완화된 격리 수준을 쓸 때보다 트랜잭션 처리량과 질의응답 시간이 크게 나빠짐
=> 원인은 부분적으로는 잠금을 획득하고 해제하는 오버헤드 때문이지만 더 중요한 원인은 동시성이 줄어드는 것임
=> 2PL을 실행하는 데이터베이스는 작업부하에 경쟁이 있다면 지연 시간이 아주 불안정하고 높은 백분위에서 매우 느릴 수 있음
=> 잠금 기반 커밋 후 읽기 격리 수준에서도 교착상태가 생길 수 있지만 2PL 직렬성 격리에서는(트랜잭션의 접근 패턴에 따라) 훨씬 더 자주 발생함

- 서술 잠금
=> 데이터베이스에 아직 존재하지 않지만 미래에 추가될 수 있는 객체(팬텀)에도 적용할 수 있음
=> 2단계 잠금이 서술잠금을 포함하면 데이터베이스에서 모든 형태의 쓰기스큐와 다른 경쟁조건을 막을 수 있어서 격리 수준이 직렬성 격리가 됨

- 색인 범위 잠금
=> 서술잠금은 잘 동작하지 않음. 진행 중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸림
=> 이 때문에 2PL을 지원하는 대부분의 데이터베이스는 실제로는 색인 범위 잠금(index range locking)/다음 키 잠금(next  key locking)을 구현함
=> 간략화한 검색 조건이 색인 중 하나에 붙음
=> 이 방법을 쓰면 팬텀과 쓰기스큐로부터 보호해주는 효과가 있음
=> 색인 범위 잠금은 서술잠금보다 정밀하지 않지만(직렬성을 유지하기 위해 반드시 필요한 것보다 큰 범위를 잠글 수도 있음) 오버 헤드가 훨씬 더 낮기 때문에 좋은 타협안이 됨

 



3.3 직렬성 스냅숏 격리(SSI)

- 알고리즘 직렬성 스냅숏 격리(serializable snapshotisolation, SSI)
=> 좋은 성능의 직렬성 격리을 가능하게 함 
=> SSI는 스냅숏 격리를 기반으로 함. 트랜잭션에서 실행되는 모든 읽기는 데이터베이스의 일관된 스냅을 보게 됨 (낙관적 동시성제어기법과 크게 다른점임)
=> 스냅숏 격리 위에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트 시킬 트랜잭션을 결정하는 알고리즘을 추가

- 비관적 동시성제어 vs 낙관적 동시성제어
=> 비관적? 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때까지 기다리는 게 낫다는 원칙을 기반
=> 낙관적? 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행함

=> 2단계잠금은 비관적 동시성제어 메커니즘 
* 직렬실행은 어떤면에서 보면 극단적으로 비관적
=> 직렬성 스냅숏 격리는 낙관적 동시성 제어기법
* 개별 트랜잭션이 아주 빨리 실행되게 해서 잠금을 유지하는 시간을 줄이는 방법으로 비관주의를 보완

- 낙관적 동시성제어
=> 오래된 아이디어고 장단점이 오랜 시간동안 논의됨
=> 경쟁이 심하면(많은 트랜잭션이 같은 객체에 접근하려고 하면) 어보트시켜야 할. 트랜잭션의 비율이 높아지므로 성능이 떨어짐
=> 시스템이 이미 최대 처리량에 근접했다면 재시도되는 트랜잭션으로부터 발생하는 부가적인 트랜잭션 부하가 성능을 저하시킬 수 있음
=> 그러나 예비용량이 충분하고 트랜잭션 사이의 경쟁이 너무 심하지 않으면 낙관적 동시성 제어 기법은 비관적 동시성 제어보다 성능이 좋은 경향이 있음

- 뒤처진 전제에 기반한 결정
=> 트랜잭션이 데이터베이스에서 어떤 데이터를 읽고 그 질의 결과를 조사한 후 관찰한 결과를 기반으로 어떤 동작(데이터베이스에쓰기)을 취할지 결정
=> 그러나 스냅숏 격리 하에서는 트랜잭션이 커밋되는 시점에 원래 질의의 결과가 더이상 최신이 아닐 수 있음. 도중에 데이터가 변경됐을 수 있기 때문
=> 데이터베이스의 질의결과 변경여부 감지 방법
1) 오래된(stale) MVCC 객체 버전을 읽었는지 감지하기(읽기 전에 커밋되지 않은 쓰기가 발생했음)
2) 과거의 읽기에 영향을 미치는 쓰기 감지하기 (읽은 후에 쓰기가 실행됨)

- 오래된 MVCC 읽기 감지하기
=> 데이터베이스는 트랜잭션이 MVCC 가시성 규칙에 따라 다른 트랜잭션의 쓰기를 무시하는 경우를 추적
=> 트랜잭션이 커밋하려고 할 때 데이터베이스는 무시된 쓰기 중에 커밋된 게 있는지 확인
=> 커밋된 게 있다면 트랜잭션은 어보트

- 과거의 읽기에 영향을 미치는 쓰기 감지하기
=> 트랜잭션이 데이터베이스에 쓸 때 영향받는 데이터를 최근에 읽은 트랜잭션이 있는지 색인에서 확인
=> 이 과정은 영향받는 키범위에 쓰기 잠금을 획득하는 것과 비슷하지만 읽는 쪽에서 커밋될 때까지 차단하지 않음
=> 트랜잭션이 읽은 데이터가 더이상 최신이 아니라고 트랜잭션에게 알려줄 뿐임

- 직렬성 스냅숏 격리의 성능
=> 2단계 잠금과 비교할 때 직렬성 스냅숏 격리의 큰 이점은 트랜잭션이 다른 트랜잭션들이 잡고 있는 잠금을 기다리느라 차단될 필요가 없다는 것임
=> 질의 지연시간 예측이 쉽고 변동이 적게 만듦. 특히 읽기 전용 질의는 어떤 잠금도 없이 일관된 스냅숏 위에서 실행될 수 있음. 읽기 작업 부하가 심한 경우에 적합