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