직렬성 격리는 가장 강력한 격리 수준
여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장
즉, 데이터베이스에서 발생할 수 있는 모든 경쟁조건을 막음
말 그대로 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하는 방식
어쩌면 단순하지만, 2007년이 되어서야 단일 스레드에서 직렬 실행하는 것이 가능해짐
Redis, 데이토믹 등에서 이와 같은 방법을 사용
데이터베이스 초창기엔 트랜잭션이 사용자 활동의 전체 흐름을 포함할 수 있게 하려고 했음
이를테면 항공권 예약의 여러 단계를 하나의 트랜잭션으로 처리되고 원자적으로 커밋된다면 깔끔할 수 있을 것이라고 생각한 것
하지만, 이는 사용자 입력을 기다려야한다는 의미이며, 이는 데이터베이스가 대부분 유휴 상태로 기다려야 한다는 의미이며, 매우 많은 동시 실행 트랜잭션을 지원해야 한다는 의미임
따라서, 누구나 알듯 요즘엔 애플리케이션 코드와 데이터베이스 서버 사이에서 질의와 결과를 주고받는 식으로 작동하고 있음
하지만, 이런 트랜잭션은 애플리케이션과 데이터베이스 사이의 네트워크 통신에 많은 시간을 소비함
즉, 데이터베이스에서 동시성을 허용하지 않는다면, 매우 낮은 처리량만이 달성될 것이며, 쓸만한 성능을 얻기 위해선 여러 트랜잭션이 동시에 실행될 수 있어야 함
따러서 단일 스레드에서 트랜잭션을 순차적으로 처리하는 시스템들은 상호작용하는 다중 구문 트랜잭션을 허용하지 않으며, 대신 스토어드 프로시저 형태의 기능을 제공함
스토어드 프로시저는 트랜잭션에 필요한 데이터는 모두 메모리에 있고, 스토어드 프로시저는 네트워크나 디스크 I/O 대기 없이 매우 빨리 실행된다고 가정
스토어드 프로시저의 단점
직렬성의 구현은 단일 장비에 있는 단일 CPU코어 속도로 트랜잭션 처리량이 제한됨
읽기 트랜잭션은 스냅숏 격리를 사용해 다른 곳에서 실행될 수 있지만, 쓰기 처리량이 높은 애플리케이션에서는 단일 스레드 트랜잭션 처리가 병목지점으로 작용될 수 있음
이를 해결하기 위해 각 CPU 코어에 각자의 파티션을 할당해서 트랜잭션 처리량을 CPU 코어 개수에 맞춰 선형적으로 확장할 수 있음
파티셔닝의 단점
여러 파티션에 접근해야 하는 트랜잭션이 있다면, 데이터베이스는 해당 트랜잭션이 접근하는 모든 파티션에 걸쳐서 코디네이션해야함(잠금, 해제)
트랜잭션이 단일 파티션에서 실행될 수 있는지 여부는 애플리케이션에서 사용되는 데이터 구조에 매우 크게 의존
모든 트랜잭션이 작고 빠를 때(하나의 트랜잭션이 모든 트랜잭션 처리를 지연시킬 수 있음)
활성화된 데이터셋이 메모리에 적재될 수 있는 경우(디스크 I/O가 발생한다면 매우 느려짐)
쓰기 처리량이 단일 CPU 코어에서 처리될 수 있을만큼 낮아야 함(그렇지 않다면, 파티셔닝을 이용할 수 있어야 함)
2PL(two-phase locking)
약 30년 동안 데이터베이스에서 직렬성을 구현하는 데 널리 쓰인 방식
잠금의 중요 개념은 트랜잭션이 동시에 같은 객체에 쓰려고 하면 잠금은 나중에 쓰는 쪽이 먼저 쓰는 쪽에서 트랜잭션을 완료할때까지 기다리도록 하는 것.
2PL에서는 쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있지만, 누군가 어떤 객체에 쓰려고(변경, 삭제)하면 독점적인 접근을 부여함
스냅숏 격리는 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않지만, 2PL은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막으며, 그 역도 성립함
구현
잠금을 공유 모드와 독점 모드로 나누어 사용
읽기 시에는 공유 모드로 잠금을 획득, 쓰기 시에는 독점 모드로 잠금을 획득
트랜잭션이 종료(커밋, 어보트)될 때까지 잠금을 획득
트랜잭션이 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드 함
공유 모드가 필요한 이유는 어떤 트랜잭션에서 읽기를 하고 있을 때 해당 객체가 변경이 되면 안되기 때문. 즉, 공유 모드 잠금을 획득했다면 다른 트랜잭션에서는 공유 모드 잠금을 획득할 수 있지만, 독점 모드 잠금은 획득할 수 없다는 의미
단점
2PL의 가장 큰 약점은 성능임
잠금을 획득하고 해제하는 오버헤드도 있지만, 더 중요한 원인은 동시성이 줄어들기 때문임
서술 잠금
앞에서 봤던 쓰기 스큐를 유발하는 팬텀 문제 즉, 한 트랜잭션이 다른 트랜잭션의 검색 질의 결과를 바꿔버리는 문제를 예를 들어 보자
회의실 예약에 있어 한 트랜잭션이 특정 시간 범위 내에 있는 회의실 예약을 검색했다면, 다른 트랜잭션은 같은 시간 범위 내에서 동일한 회의실을 예약하거나 갱신할 수 없음
이는 개념상으로 서술 잠금이 필요함
SELECT * FROM bookings
WHERE room_id = 123 AND
end_time > '2018-01-01 12:00' AND
start_time < '2018-01-01 13:00';
색인 범위 잠금
하지만 서술 잠금은 조건에 부합하는 잠금을 확인하는 데 오랜 시간이 걸림
따라서 2PL을 지원하는 데이터베이스는 실제로는 색인 범위 잠금을 구현함
일종의 서술 잠금을 간략하게 근사한 것으로, 조건을 색인 범위로 간략하게 계산해 잠금을 실행함
범위 잠금과 대치되는 적합한 색인이 없다면 테이블 전체를 공유 잠금으로 잡음
2008년에 처음 등장
단일 노드 데이터베이스(PostgreSQL 9.1 이상)와 분산 데이터베이스(파운데이션DB) 모두에서 사용
완전한 직렬성을 제공하며, 스냅숏 격리에 비해 약간의 성능 손해를 가짐
비관적 동시성 제어
낙관적 동시성 제어
위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 계속 진행하도록 하는 것
최종적으로 트랜잭션이 커밋되어야 할 때 데이터베이스는 나쁜 상황이 발생했는지(격리가 위반되었는지)를 확인하며, 위반됐다면 어보트시킴
하지만, 경쟁이 심하면 어보트시켜야 할 트랜잭션 비율이 높아져 성능이 저하됨
SSI는 스냅숏 격리 위에서 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트시킬 트랜잭션을 결정하는 알고리즘이 작동
예비 용량이 충분하며 트랜잭션 사이의 경쟁이 너무 심하지 않으면, 낙관적 동시성 제어 기법이 비관적 동시성 제어보다 성능이 좋은 경향이 있음
앞선 스냅숏 격리에서 쓰기 스큐를 설명할 때 반복되는 패턴이 있음
트랜잭션이 데이터베이스에서 데이터를 읽고 그 질의 결과를 기반으로 어떤 동작을 취할지 결정함. 하지만 스냅숏 격리하에서는 트랜잭션이 커밋되는 시점에 질의의 결과가 더 이상 최신이 아닐 수 있음
직렬성 격리를 제공하려면 데이터베이스는 트랜잭션이 뒤처진 전제를 기반으로 동작하는 상황을 감지하고 그런 상황에서는 트랜잭션을 어보트시켜야 함
이를 위해선 두 가지 상황을 고려해야 함
오래된 MVCC 객체를 읽었는지 감지
과거의 읽기에 영향을 미치는 쓰기 감지(읽은 후에 쓰기가 실행되는지)
스냅숏 격리에서는 트랜잭션이 MVCC 데이터베이스의 일관된 스냅숏에서 읽으면 스냅숏 생성 시점에 다른 트랜잭션이 썼지만 아직 커밋되지 않은 데이터는 무시함
하지만 이와 같은 방식을 사용하면, 위 그림에서 처럼 오류가 발생함
이를 위해서는 오래된 읽기(이전 버전의 MVCC 스냅숏)가 감지되었을 때 데이터베이스는 무시된 쓰기 중에 커밋된 것이 있는지 확인해야함