데이터 중심 애플리케이션 설계 7장 (트랜잭션) 정리

백종현·2023년 9월 12일
0

데이터 시스템은 여러 가지 문제 (비정상적인 오류나 실패)가 일어날 수 있다.

이러한 문제를 단순화한 메커니즘으로 트랜잭션이 채택돼 왔다.

이를 당연한 것으로 여기면 안된다. 트랜잭션은 자연 법칙이 아니며, 데이터베이스에 접속하는 애플리케이션에서 프로그래밍 모델을 단순화하려는 목적으로 만들었다. 트랜잭션을 통해 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있다.

이번 장에서는 문제가 생길 수 있는 예를 조사하고, 이런 문제를 방지하기 위한 데이터베이스 알고리즘을 살펴본다.

ACID의 의미

ACID는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질이다.

원자성

데이터베이스는 전부 반영되거나 아무것도 반영되지 않는 것을 보장하는 것으로써 원자성을 통해 부분 갱신으로 더 큰 문제가 야기되는 것을 방지할 수 있다.

일관성

트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 말한다.

일관성의 아이디어는 항상 진실이어야 하는, 데이터에 관한 어떤 선언(불변식)이 있다는 것이다.

일관성(C)은 실제로는 ACID에 속하지 않고 애플리케이션의 속성으로 본다.
데이터베이스 자체만으로 불변식을 위반하는 잘못된 데이터를 쓰지 못하도록 막을 수 없기 때문이다.
이러한 것은 애플리케이션의 책임으로 보고 일관성을 달성하기 위해 데이터베이스의 원자성과 격리성 속성에 기댈 수 있다.

격리성

동시에 실행되는 트랜잭션은 서로 격리되어 방해할 수 없는 것을 의미한다. 트랜잭션은 다른 트랜잭션을 방해할 수 없다.

한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼수 없든지 둘 중 하나여야 하고 일부분만 볼 수 있어서는 안된다.

데이터베이스는 실제로 여러 트랜잭션이 동시에 실행되더라도 트랜잭션이 커밋됐을 때의 결과가 트랜잭션이 순차적으로 실행됐을 떄의 결과와 동일하도록 보장한다.

그러나 직렬성 격리는 성능 손해를 동반하므로, 거의 사용되지 않는다.

지속성

데이터베이스 시스템의 목적은 데이터를 잃어버릴 염려가 없는 안전한 저장소를 제공하는 것이다.

지속성은 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장이다.

오라클, MySQL의 경우 REDO 로그(쓰기 전 로그)를 활용하여 지속성을 구현했다.

오류와 어보트 처리

트랜잭션의 핵심 기능이다. 오류가 생기면 어보트되고 안전하게 재시도할 수 있다. ACID는 이 철학을 바탕으로 한다.

하지만, 모든 시스템이 이를 따르지는 않으며, 리더 없는 복제는 데이터스토어는 "최선을 다하는"(best-effort) 원칙으로, 오류가 발생하면 이미 한 일은 취소하지 않는다. 애플리케이션에서 오류 처리를 해야 한다.

Rails의 액티브레코드, Django ORM 등은 어보트된 트랜잭션을 재시도하지 않는다.

어보트된 트랜잭션을 재시도하는 것은 간단하고 효과적인 오류 처리 메커니즘이지만 완벽하지 않다.

  • 커밋 성공 네트워크가 끊겼을 때, 클라이언트가 성공인지 알 수 없음
  • 일시적인 오류만 재시도할 가치가 있고, 영구적 오류는 재시도해도 소용없다.

완화된 격리 수준

커밋 후 읽기 (READ COMMITTED)

가장 기본적인 수준의 트랜잭션 격리로 이 수준에서는 두 가지를 보장해 준다.

데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다(더티 읽기가 없음)
데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다(더티 쓰기가 없음)

더티 읽기 방지

더티 읽기(dirty read) : 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상

그림 7-2. 격리성 위반: 트랜잭션이 다른 트랜잭션에서 썼지만 커밋되지 않는 데이터를 읽음"(더티 읽기(dirty read))"

더티 읽기를 막는게 유용한 이유

  • 더티 읽기가 생기면 다른 트랜잭션이 일부는 갱신된 값을, 일부는 갱신되지 않은 값을 볼 수 있다.
  • 트랜잭션이 어보트되면 모두 롤백되어야 하나, 더티 읽기를 허용하면 트랜잭션이 나중에 롤백될 데이터를 볼 수 있다.

더티 쓰기 방지

더티 쓰기(dirty write) : 두 트랜잭션이 동일한 객체를 동시에 갱신하려고 할 때, 먼저 쓴 내용이 아직 커밋되지 않은 트랜잭션에서 쓴 것이고 나중에 실행된 쓰기 작업이 커밋되지 않은 값을 덮는 경우

그림 7-5. 다른 트랜잭션에서 충돌하는 쓰기를 실행할 때 더티 쓰기가 있으면 내용이 섞일 수 있다.

위 예시를 보면, 밥에게 판매 됐지만, 송장은 앨리스에게 간다.

커밋 후 읽기 구현

커밋 후 읽기는 Oracle 11g, PostgreSQL, SQL Server 2012, MemSQL 등에서 기본 설정으로 쓰고 있는 격리 수준이며, 더티 쓰기와 더티 읽기를 방지한다.

  • 더티 쓰기 방지 : 트랜잭션이 커밋되거나 어보트될 때까지 잠금을 보유한다. 이런 잠금은 커밋 후 읽기 모드에서 데이터베이스에 의해 자동으로 실행된다.

  • 더티 읽기 방지: 과거의 커밋된 값/ 현재 쓰고 있는 새로운 값을 모두 기억하게 하여 해당 트랜잭션이 실행 중인 동안 과거의 값을 읽게하여 더티 읽기를 방지 할 수 있다.

더티 읽기도 잠금을 통해 할 수 있으나, 성능 상 문제 때문에, 위 방식을 사용한다.

스냅숏 격리와 반복 읽기


그림 7-6. 읽기 스큐: 앨리스는 일관성이 깨진 상태인 데이터베이스를 본다. (본인 계좌1에서 계좌2로 100달러 옮기는 예시)

위 경우, 엘리스는 1000달러가 들어있으어야하는 잔고에, 현재 계좌 총액이 500 + 400으로 총 900 달러만 있는 것처럼 보인다.

커밋 후 읽기 격리 수준에서도 동시성 버그가 생길 수 있으며 이런 현상을 비반복 읽기(nonrepeatable read)나 읽기 스큐(read skew)라고 한다.

위와 같은 경우 몇 초 후 새로고침하면 일관성 있는 계좌를 볼 수 있으나 어떤 상황에서는 이런 비일관성을 감내할 수 없는 경우도 있다.

  • 백업 : 원본과 복사본의 데이터 차이. 잘못된 백업본을 사용해 복원하면 사라진 돈 같은 비일관성이 영속적이여질 수 있음.
  • 분석 질의와 무결성 확인 : 큰 부분을 스캔하는 질의시 다른 시점의 데이터베이스 일부를 보게되면 잘못된 결과 반환. -> 예를 들어 1시간에 한번씩 하는 분석 질의 시 잘못된 결과 도출

따라서, 스냅숏 격리를 통해 해결한다.

스냅숏 격리 구현

성능 관점에서 읽는 쪽에서 쓰는 쪽을 결코 차단하지 않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않는다. 잠금이 필요없다.

다중 버전 동시성 제어(multi-version concurrency control, MVCC) : 데이터베이스가 객체의 여러 버전을 함께 유지하는 기법

  1. 트랜잭션마다 ID를 할당, 삭제 요청 트랜잭션도 마찬가지
  2. 트랜잭션 ID가 더 크거나 ID를 할당받을 시점에 진행중인 모든 트랜잭션이 쓴 데이터가 아닌 데이터를 읽음
  3. 나중에 아무 트랜잭션도 접근하지 않는다는게 확실해지면 가비지 컬렉션 프로세스가 오래된 객체 버전을 삭제 -> 이때 색인 항목도 삭제됨


그림. MySQL MVCC 동작 방식

트랜잭션 ID가 더 큰(즉 현재 트랜잭션이 시작한 후에 시작한) 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계 없이 모두 무시된다

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

아래 두 조건이 참이면, 객체를 볼 수 있다.

  • 읽기를 실행하는 트랜잭션이 시작한 시점에 읽기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태
  • 읽기 대상 객체가 삭제된 것으로 표시되지 않았거나, 삭제된 것으로 표시됐지만 읽기를 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 커밋되지 않은 경우

색인과 스냅숏 격리

다중 버전 데이터베이스에서 색인의 동작
색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내고, 가비지 컬렉션이 어떤 트랜잭션에게도 더 이상 보이지 않는 오래된 객체 버전을 삭제 할때 대응되는 색인 항목도 삭제

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

스냅숏 격리는 읽기 전용 트랜잭션에서 유용하며, SQL 표준에 스냅숏 격리의 개념이 없기 때문에 여러 데이터베이스에서 다른 이름으로 불린다.

Oracle: 직렬성(Serializable)
PostgreSQL, MySQL: 반복 읽기(Repeatable Read)

갱신 손실 방지

지금까지 커밋 후 읽기와 스냅숏 격리 수준은 동시에 실행되는 쓰기 작업이 있을 때 읽기 전용 트랜잭션이 무엇을 볼 수 있는지에 대한 보장과 관련된 것이었다.

두 트랜잭션이 동시에 쓰기를 실행할 때의 문제는 거의 무시했다.

만약 두 트랜잭션이 작업을 동시에 하면 두번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있음

이 패턴은 다양하게 발생한다.

  • 카운터를 증가시키거나, 계좌 잔고를 갱신한다.
  • JSON 문서 내에 있는 리스트에 엘리먼트를 추가한다.
  • 노션 같이 두명의 사용가자 동시에 같은 페이지를 편집한다.

이 문제들을 해결하기 위해 아래와 같은 해결책들이 개발되었다.
1. 원자적 쓰기 연산
2. 명시적인 잠금
3. 갱신 손실 자동 감지
4. Compare-and-set
5. 충돌 해소와 복제

1. 원자적 쓰기 연산

  • 쓰기 연산에 원자성 (Atomicity) 성질을 부여함으로서 동시성 안전 획득
  • exclusive lock 을 획득하여 구현 → 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못함
  • 모든 원자적 연산을 단일 스레드에서 실행되도록 강제하는 방법

2. 명시적인 잠금

  • 애플리케이션에서 갱신할 객체를 명시적으로 잠그는 것
  • 다른 트랜잭션이 동시에 같은 객체를 읽으려고 하면 첫 번째 read-modify-write 주기가 완료될 때까지 기다리도록 강제됨

예제 7-1. 로우를 명시적으로 잠금으로써 갱신 손실 막기

BEGIN TRANSACTION;

SELECT * FROM figures
  WHERE name = 'robot' AND game_id = 222
  FOR UPDATE; (1)

-- 이동이 유효한지 확인한 후
-- 이전의 SELECT에서 반환된 것의 위치를 갱신한다.
UPDATE figures SET position = '4' WHERE id = 1234;

COMMIT;

3. 갱신 손실 자동 감지

여러 트랜잭션의 병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 abort 시키고, 재시도하도록 강제하는 방법

MySQL은 이를 제공하지 않는다.

갱신 손실 감지는 애플리케이션 코드에서 잠금이나 원자적 연산을 쓰는 것을 잊어버리더라도 오류를 덜 발생하게 해준다.

4. Compare-and-set

값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용함으로써 갱신 손실을 회피하는 것

예를 들어, 두 사용자가 노션 페이지를 갱신하지 못하도록 이런 방법을 시도할 수 있다. 사용자가 페이지 편집을 시작한 이후로 내용이 바뀌지 않았을 때만 갱신되게 하는 것이다.

-- 데이터베이스 구현에 따라 안전할 수도 안전하지 않을 수도 있다
UPDATE wiki_pages SET content = 'new content'
  WHERE id = 1234 AND content = 'old content';

내용이 바뀌어서 더는 'old content'와 일치하지 않으면 갱신이 적용되지 않는 것이다.

5. 충돌 해소와 복제

  • 잠금과 compare-and-set 연산은 데이터의 최신 복사본이 하나만 있다고 가정함
  • 다중 리더 또는 리더 없는 복제를 사용하는 데이터베이스는 일반적으로 여러 쓰기가 동시에 실행되고 비동기식으로 복제되는 것을 허용함
  • 따라서 데이터의 최신 복사본이 하나만 있으리라고 보장할 수 없음
  • 복제가 적용된 데이터베이스에서 흔히 쓰는 방법은 쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전을 생성하는 것을 허용하고, 추후에 충돌을 해소하고 이 버전들을 병합함

보통 최종 쓰기 승리 (last write wins, LWW)를 사용해 갱신 손실이 일어난다.

쓰기 스큐와 팬텀

거의 동시에 두 트랜잭션이 시작되었다고 가정

데이터베이스에서 스냅숏 격리를 사용하므로 둘 다 2를 반환해서 두 트랜잭션 모두 다음 단계로 진행함
최소 한 명의 의사가 호출 대기해야 한다는 요구사항 위반
이러한 현상을 쓰기 스큐 (wirte skew) 라고 함

  • 쓰기 스큐가 일어나는 경우는 두 트랜잭션이 같은 객체들을 읽어서 그 중 일부를 갱신할 때 나타날 수 있다. (SELECT 후, UPDATE나 INSERT시)

추가적인 쓰기 스큐의 예

  • 회의실 예약 시스템
  • 다중플레이어 게임
  • 사용자명 획득 (중복 사용자명 막기)
  • 이중 사용 방지 (잔고가 음수가 되지 않도록 막기)

쓰기 스큐를 유발하는 팬텀


1. SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색함으로써 어떤 요구사항을 만족하는지 확인
2. 첫 번째 질의의 결과에 따라 애플리케이션 코드는 어떻게 진행할지 결정
3. 애플리케이션이 계속 처리하기로 결정했다면 데이터베이스에 쓰고 트랜잭션을 커밋한다. 이 쓰기의 효과로 2단계를 결정한 전제조건이 바뀐다.

어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 것을 팬텀(Phantom) 이라고 함

충돌 구체화

INSERT 시에는 잠금을 적용하지 못한다. 이는 최초의 select 시 잠글 수 있는 객체가 없기 때문이었다.
1. 인위적으로 데이터베이스에 잠금 객체를 추가하자
2. 대상 row 를 미리 만들고 lock 을 건다. 트랜잭션 대상이 되는 특정 범위의 모든 조합에 대해 미리 row 를 만들어 둠 (ex, 회의실 예약의 경우 다음 6개월 동안에 해당되는 양)
3. 예약을 하는 트랜잭션은 테이블에서 원하는 대상 row 를 잠글 수 있음 (위에서 미리 생성했기 때문에)

이렇게 미리 만들어 놓는 방식을 충돌 구체화라고 한다.

생성된 row 는 단지 동시에 변경되는 것을 막기 위한 잠금의 모음일 뿐이다. 실제 사용되는 데이터가 아니다.
단점은 동시성 제어 메커니즘이 애플리케이션 데이터모델로 새어 나오는 것은 보기 좋지 않다. 다른 대안이 불가능할 때 최후의 수단으로 고려

직렬성

DB의 동시성을 관리하는 방식의 문제점

  • 격리 수준은 이해하기 어렵고 데이터베이스마다 그 구현에 일관성이 없음
  • 애플리케이션 코드를 보고 특정한 격리 수준에서 해당 코드를 실행하는게 안전한지 알기 어려움, 특히 동시에 일어나는 모든 일을 알지 못할 수도 있는 거대한 애플리케이션이라면 더 그렇다.
  • 동시성 문제는 보통 비결정적(간헐적) 이라서 테스트하기 어려움. 운이 나쁠 때만 문제가 발생하게 됨

대안은 직렬성 격리 사용이다. 직렬성 격리는 보통 가장 강력한 격리 수준이라고 여겨진다.
여러 트랜잭션이 병렬로 실행되더라도, 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장

실제적인 직렬 실행

실제로 볼드DB/H-스토어, 레디스, 데이토믹의 경우는 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하고, 이는 잠금을 위한 오버헤드를 피할 수 있다. 문제는 CPU 코어 하나의 처리량 제한이라는 성능적인 문제가 있다.

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

  • 데이터베이스 초창기에는 트랜잭션이 사용자의 활동 전체 흐름을 포함할 수 있게 하려는 의도가 있었음
  • 항공권 예약의 여러 과정 (경로 선택, 요금, 가용 좌석 탐색, 여행 일정표 정하기, …) 을 하나의 트랜잭션으로 표현하고 원자적으로 커밋하는 것임
  • 이 방법을 구현하기 위해 데이터베이스 트랜잭션이 사용자의 입력을 기다려야 한다면, 매우 느릴 것으로 예상됨
  • 대신에 트랜잭션 코드 전체를 스토어드 프로시저 형태로 데이터베이스에 미리 제출함
  • 트랜잭션에 필요한 데이터는 모두 메모리에 있고, 스토어드 프로시저는 네트워크나 디스크 I/O 없이 매우 빨리 실행된다고 가정함

스토어 프로시져의 장단점

  • 데이터베이스 벤더마다 제각각의 프로시저용 언어가 있다 (오라클은 PL/SQL, SQL 서버는 T-SQL, PostgreSQL은 PL/pgSQL)
  • 데이터베이스에서 실행되는 코드는 관리하기 어렵다.
  • 데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감할 떄가 많다.

그러나 이런 문제는 범용 프로그래밍 언어를 사용하여 극복할 수 있다.
볼트 DB -> Java, Groovy
레디스 -> Lua Script

파티셔닝

각 트랜잭션이 단일 파티션 내에서만 데이터를 읽고 쓰도록 파티셔닝 할 수 있다면, 각 파티션은 다른 파티션과 독립적으로 실행되는 자신만의 트랜잭션 처리 스레드를 가질 수 있다. 이 경우 각 CPU 코어에 각자의 파티션을 할당해서 트랜잭션 처리량을 CPU 코어 개수에 맞춰 선형적으로 확장할 수 있다.
그러나 여러 파티션에 접근해야 하는 트랜잭션이 있다면, 코디네이션 오버헤드가 있으므로 단일 파티션 트랜잭션보다 엄청 느리다

직렬 실행 요약

트랜잭션 직렬 실행은 몇 가지 제약 사항 안에서 직렬성 격리를 획득하는 시용적인 방법이 됐음

  • 모든 트랜잭션은 작고 빨라야 한다. 느린 트랜잭션 하나가 전체 처리를 지연시킬 수 있기 때문.
  • 활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한됨, 단일 스레드 트랜잭션에서 디스크에 접근한다면 시스템이 매우 느려짐
  • 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 함
  • 여러 파티션에 걸친 트랜잭션도 쓸 수 있지만, 이것을 사용할 수 있는 정도에는 엄격한 제한이 있음

2단계 잠금

쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있다. 하지만, 누군가 어떤 객체에 쓰려고 하면 독점적 접근이 필요하다.

  • 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰기를 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다(이렇게 하면 B가 A 몰래 갑자기 객체를 변경하지 못하도록 보장된다).

  • 트랜잭션 A가 객체에 썼고 트랜잭션 B가 그 객체를 읽기 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다(그림 7-4에 나왔듯이 2PL을 쓸 때는 객체의 과거 버전을 읽는 게 허용되지 않는다).

2단계 잠금 구현

  • MySQL, SQL Server 에서 직렬성 격리 수준을 구현하는데 사용됨
  • 잠금은 공유 모드 (shared mode) 나 독점 모드 (exclusive mode) 로 사용될 수 있음
  • 잠금이 아주 많이 사용되므로 교착 상태(두 개의 트랜잭션이 서로 기다리는 것)가 매우 쉽게 발생할 수 있음

2단계 잠금의 성능

  • 가장 큰 약점이 성능
  • 잠금을 획득하고 해제하는 오버헤드 발생
  • 동시성이 줄어들어 성능 저하. (동시성과 성능은 반비례)

서술 잠금

조건에 부합하는 모든 객체에 잠금을 획득하는 것

SELECT * FROM bookings
   WHERE room_id = 123 AND
      end_time > '2018-01-01 12:00' AND
      start_time < '2018-01-01 13:00'

서술 잠금은 오래 걸림 (조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸림)
이 때문에 2PL 을 지원하는 대부분의 데이터베이스는 실제로는 색인 범위 잠금, 다음 키 잠금을 구현하여 사용함

색인 범위 잠금

  • 예를 들어, 정오와 오후 1시 사이에 123번 방을 예약하는 것에 대한 서술 잠금을 → 모든 시간 범위에 123번 방을 예약하는 것으로 근사시켜 잠금 실행
  • 위의 예시에서 room_id 또는 시간 값에 색인이 걸려있을 것이기에 해당 색인 범위에 lock 을 거는 것임
  • 색인 범위 잠금은 서술 잠금 보다 정밀하지 않지만(직렬성을 유지하기 위해 반드시 필요한 것보다 더 큰 범위를 잠글 수도 있음) 오버헤드가 낮기 때문에 좋은 타협안이 됨
  • 범위 잠금을 잡을 수 있는 적합한 색인이 없다면 테이블 전체에 공유 잠금을 잡는 것으로 대체

직렬성 스냅숏 격리 (Serializable Snapshot Isolation, SSI)

  • 직렬성 격리와 좋은 성능은 공존할 수 있을까?
  • 현재 최고로 유망한 것이 직렬성 스냅숏 격리임
  • 스냅숏 격리에 비해 약간의 성능 손해만 있을 뿐임

비관적 동시성 제어 vs 낙관적 동시성 제어

2단계 잠금은 비관적 동시성 제어 메커니즘임

  • 뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때 까지 기다리는게 낫다는 원칙

직렬성 스냅숏 격리는 낙관적 동시성 제어 메커니즘임

  • 뭔가 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행한다는 뜻
  • 트랜잭션이 커밋되기를 원할 때 데이터베이스는 나쁜 상황이 발생했는지 확인함
  • 발생했다면 abort 되고 재시도함
  • 경쟁이 심하면 abort 비율이 높아지므로 성능 떨어짐

오래된 MVCC 읽기 감지하기


그림 7-10. 트랜잭션이 MVCC 스냅숏에서 뒤처지 값을 읽었는지 감지하기

트랜잭션 43이 커밋하기 원할 때 트랜잭션 42가 이미 커밋된 상태이고, 무시됐던 쓰기가 지금은 영향이 있고 트랜잭션 43의 전제가 더이상 참이 아니라는 뜻이다.

과거의 읽기에 영향을 미치는 쓰기 감지하기

그림 7-11. 직렬성 스냅숏 격리에서 트랜잭션이 다른 읽은 데이터를 변경하는 경우를 감지하기

profile
노력하는 사람

0개의 댓글