이 페이지에서는 Spanner의 트랜잭션을 설명하고 Spanner의 읽기-쓰기, 읽기 전용, Partitioned DML 트랜잭션 인터페이스를 소개합니다.
Spanner의 트랜잭션은 데이터베이스의 열, 행, 테이블 전체에 걸쳐 단일 논리 시점에서 원자적으로 실행되는 읽기 및 쓰기 집합입니다.
세션은 Spanner 데이터베이스에서 트랜잭션을 수행하는 데 사용됩니다. 세션은 Spanner 데이터베이스 서비스와의 논리적 통신 채널을 나타냅니다. 세션은 한 번에 하나 또는 여러 개의 트랜잭션을 실행할 수 있습니다. 자세한 내용은 세션을 참조하세요.
트랜잭션 유형
Spanner는 각각 특정 데이터 상호작용 패턴에 맞게 설계된 다음과 같은 트랜잭션 유형을 지원합니다.
최적의 동시 실행 제어 및 비관적 동시 실행 제어
- 읽기-쓰기 스냅샷 격리 트랜잭션: 이러한 트랜잭션은 차단을 사용하지 않고 대신 트랜잭션이 시작되었을 때 존재했던 데이터베이스의 자체 '스냅샷'에서 트랜잭션을 진행합니다. 충돌은 커밋 시점에만 감지되고 해결됩니다.
- 읽기-쓰기 잠금: 이러한 트랜잭션은 비관적 잠금과 필요한 경우 2단계 커밋을 사용합니다. 실패하여 재시도가 필요할 수 있습니다. 단일 데이터베이스로 제한되지만 해당 데이터베이스 내 여러 테이블에서 데이터를 수정할 수 있습니다.
읽기 전용: 이러한 트랜잭션은 여러 읽기 작업에서 데이터 일관성을 보장하지만 데이터 수정을 허용하지 않습니다. 일관성을 위해 시스템에서 결정한 타임스탬프 또는 사용자가 구성한 이전 타임스탬프에서 실행됩니다. 읽기-쓰기 트랜잭션과 달리 커밋 작업이나 잠금이 필요하지 않지만 진행 중인 쓰기 작업이 완료될 때까지 일시중지하여 기다릴 수 있습니다.
Partitioned DML: 이 트랜잭션 유형은 DML 문을 Partitioned DML 작업으로 실행합니다. 데이터 정리 또는 일괄 데이터 삽입과 같은 대규모 데이터 업데이트 및 삭제에 최적화되어 있습니다. 원자적 트랜잭션이 필요하지 않은 다수의 쓰기의 경우 일괄 쓰기를 사용하는 것이 좋습니다. 자세한 내용은 일괄 쓰기를 사용하여 데이터 수정을 참고하세요.
읽기-쓰기 트랜잭션
읽기-쓰기 잠금 트랜잭션을 사용하여 데이터베이스의 모든 위치에서 데이터를 원자적으로 읽고, 수정하고, 쓸 수 있습니다. 이 유형의 트랜잭션은 외적으로 일관됩니다.
트랜잭션이 활성 상태인 시간을 최소화합니다. 트랜잭션 기간이 짧을수록 커밋 성공 확률이 높아지고 경합이 줄어듭니다.
Spanner는 트랜잭션이 계속 읽기를 수행하고 트랜잭션이 sessions.commit
또는 sessions.rollback
작업을 통해 종료되지 않는 한 읽기 잠금을 활성 상태로 유지하려고 시도합니다.
클라이언트가 장시간 비활성 상태로 있으면 Spanner에서 트랜잭션의 잠금을 해제하고 트랜잭션을 중단할 수 있습니다.
개념적으로 읽기-쓰기 트랜잭션은 0개 이상의 읽기 또는 SQL 문과 그 뒤에 오는 sessions.commit
으로 구성됩니다. sessions.commit
전에 언제든지 클라이언트는 트랜잭션을 중단하기 위해 sessions.rollback
요청을 보낼 수 있습니다.
하나 이상의 읽기 작업에 종속되는 쓰기 작업을 수행하려면 읽기-쓰기 잠금 트랜잭션을 사용하세요.
- 하나 이상의 쓰기 작업을 원자적으로 커밋해야 하는 경우 동일한 읽기-쓰기 트랜잭션 내에서 해당 쓰기를 수행합니다. 예를 들어 계좌 A에서 계좌 B로 200달러를 이체하는 경우 동일한 트랜잭션 내에서 쓰기 작업(계좌 A를 200달러 줄이고 계좌 B를 200달러 늘림)과 초기 계좌 잔액 읽기를 모두 수행합니다.
- 계좌 A의 잔액을 두 배로 늘리려면 동일한 트랜잭션 내에서 읽기 및 쓰기 작업을 수행합니다. 이렇게 하면 시스템이 잔액을 두 배로 늘린 후 업데이트하기 전에 잔액을 읽습니다.
- 하나 이상의 읽기 작업의 결과에 따라 쓰기 작업을 한 개 이상 수행할 수 있는 경우, 쓰기 작업이 실행되지 않더라도 동일한 읽기-쓰기 트랜잭션에서 해당 쓰기 및 읽기 작업을 수행합니다. 예를 들어 계좌 A의 현재 잔액이 500달러를 초과하는 경우에만 계좌 A에서 계좌 B로 200달러를 이체하려면 이체가 발생하지 않더라도 계좌 A의 잔액 읽기와 조건부 쓰기 작업을 동일한 트랜잭션 내에 포함합니다.
읽기 작업을 수행하려면 단일 읽기 메서드 또는 읽기 전용 트랜잭션을 사용하세요.
- 읽기 작업만 수행하고 단일 읽기 메서드를 사용하여 읽기 작업을 표현할 수 있는 경우, 해당 단일 읽기 메서드 또는 읽기 전용 트랜잭션을 사용합니다. 단일 읽기는 읽기-쓰기 트랜잭션과 달리 잠금을 획득하지 않습니다.
인터페이스
Spanner 클라이언트 라이브러리는 읽기-쓰기 트랜잭션 내에서 작업 본문을 실행하기 위한 인터페이스를 제공하며, 트랜잭션 중단을 위한 재시도도 제공합니다. Spanner 트랜잭션을 커밋하기 전에 여러 번 재시도해야 할 수 있습니다.
트랜잭션이 중단되는 상황은 여러 가지가 있습니다. 예를 들어 두 트랜잭션이 동시에 데이터를 수정하려고 하면 교착 상태가 발생할 수 있습니다. 이 경우 Spanner는 한 트랜잭션을 중단하여 다른 트랜잭션이 진행되도록 합니다. 드물지만 Spanner 내의 일시적인 이벤트로 인해 트랜잭션이 중단될 수도 있습니다.
트랜잭션은 원자적이므로 중단된 트랜잭션은 데이터베이스에 영향을 미치지 않습니다. 동일한 세션 내에서 트랜잭션을 다시 시도하여 성공률을 개선합니다. ABORTED
오류가 발생하는 각 재시도마다 트랜잭션의 잠금 우선순위가 증가합니다.
Spanner 클라이언트 라이브러리에서 트랜잭션을 사용하는 경우, 트랜잭션의 본문을 함수 객체로 정의합니다. 이 함수는 하나 이상의 데이터베이스 테이블에서 수행되는 읽기 및 쓰기를 캡슐화합니다. Spanner 클라이언트 라이브러리는 트랜잭션이 성공적으로 커밋되거나 재시도할 수 없는 오류가 발생할 때까지 이 함수를 반복적으로 실행합니다.
예
Albums
테이블에 MarketingBudget
열이 있다고 가정해 보겠습니다.
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
마케팅 부서에서 Albums
(2, 2)
의 예산에서 Albums (1, 1)
로 $200,000를 옮겨 달라고 요청했습니다. 단, 해당 앨범의 예산 내에서만 이 금액을 사용할 수 있습니다. 읽기 결과에 따라 트랜잭션이 쓰기를 수행할 수 있으므로 이 작업에 읽기-쓰기 잠금 트랜잭션을 사용해야 합니다.
다음은 읽기-쓰기 트랜잭션을 실행하는 방법을 보여줍니다.
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
시맨틱스
이 섹션에서는 Spanner의 읽기-쓰기 트랜잭션의 시맨틱스를 설명합니다.
속성
Spanner의 읽기-쓰기 트랜잭션은 일련의 읽기와 쓰기를 원자적으로 실행합니다. 읽기-쓰기 트랜잭션이 실행되는 타임스탬프는 경과 시간과 일치합니다. 직렬화 순서는 이 타임스탬프 순서와 일치합니다.
읽기-쓰기 트랜잭션은 관계형 데이터베이스의 ACID 속성을 제공합니다. Spanner 읽기-쓰기 트랜잭션은 일반적인 ACID보다 강력한 속성을 제공합니다.
이러한 속성으로 인해 애플리케이션 개발자는 동시에 실행될 수 있는 다른 트랜잭션에서 해당 실행을 보호할 방법을 걱정하지 않고 각 트랜잭션의 정확성에만 집중할 수 있습니다.
읽기-쓰기 트랜잭션 격리
일련의 읽기 및 쓰기가 포함된 트랜잭션을 성공적으로 커밋하면 다음과 같이 표시됩니다.
- 트랜잭션은 트랜잭션의 커밋 타임스탬프에서 일관된 스냅샷을 반영하는 값을 반환합니다.
- 커밋 시점에 빈 행이나 범위가 비어 있습니다.
- 트랜잭션은 트랜잭션의 커밋 타임스탬프에서 모든 쓰기를 커밋합니다.
- 트랜잭션이 커밋될 때까지는 어떤 트랜잭션도 쓰기를 볼 수 없습니다.
Spanner 클라이언트 드라이버에는 트랜잭션을 다시 실행하고 클라이언트가 관찰하는 데이터를 검증하여 일시적인 오류를 마스킹하는 트랜잭션 재시도 로직이 포함됩니다.
트랜잭션 자체의 관점과 Spanner 데이터베이스에 대한 다른 판독기와 기록기의 관점 모두에서 모든 읽기와 쓰기가 단일 시점에서 발생한 것으로 보이는 효과가 있습니다. 즉, 읽기와 쓰기가 동일한 타임스탬프에서 발생합니다. 예시를 보려면 직렬화 가능성 및 외부 일관성을 참고하세요.
읽기 트랜잭션 격리
읽기-쓰기 트랜잭션이 읽기 작업만 수행하면 읽기 전용 트랜잭션과 유사한 일관성 보장을 제공합니다. 트랜잭션 내의 모든 읽기는 존재하지 않는 행의 확인을 포함하여 일관된 타임스탬프의 데이터를 반환합니다.
한 가지 차이점은 읽기-쓰기 트랜잭션이 쓰기 작업을 실행하지 않고 커밋하는 경우입니다. 이 시나리오에서는 트랜잭션 내에서 읽은 데이터가 읽기 작업과 트랜잭션 커밋 간에 데이터베이스에서 변경되지 않았음을 보장할 수 없습니다.
데이터 최신 상태를 보장하고 마지막 검색 이후 데이터가 수정되지 않았는지 확인하려면 후속 읽기가 필요합니다. 이 재읽기는 다른 읽기-쓰기 트랜잭션 내에서 또는 강력 읽기로 수행할 수 있습니다.
최적의 효율성을 위해 트랜잭션이 읽기만 수행하는 경우 읽기-쓰기 트랜잭션 대신 읽기 전용 트랜잭션을 사용하세요.
원자성, 일관성, 복구 가능성
Spanner는 격리 외에도 다음과 같은 다른 ACID 속성 보장을 제공합니다.
- 원자성. 트랜잭션의 모든 작업이 성공적으로 완료되거나 전혀 완료되지 않으면 트랜잭션이 원자적이라고 간주됩니다. 트랜잭션 내의 작업이 실패하면 전체 트랜잭션이 원래 상태로 롤백되어 데이터 무결성이 보장됩니다.
- 일관성. 트랜잭션은 데이터베이스의 규칙 및 제약 조건의 무결성을 유지해야 합니다. 트랜잭션이 완료되면 데이터베이스는 사전 정의된 규칙을 준수하는 유효한 상태여야 합니다.
- 내구성. 트랜잭션이 커밋되면 변경사항이 데이터베이스에 영구적으로 저장되며 시스템 장애, 정전 또는 기타 중단이 발생해도 유지됩니다.
직렬 가능성 및 외적 일관성
Spanner는 직렬화 가능성 및 외적 일관성을 비롯한 강력한 트랜잭션 보장을 제공합니다. 이러한 속성을 통해 분산 환경에서도 데이터가 일관되게 유지되고 작업이 예측 가능한 순서로 실행됩니다.
직렬화 가능성을 통해 모든 트랜잭션이 동시에 처리되더라도 단일 순차적 순서로 하나씩 실행되는 것처럼 보입니다. Spanner는 커밋된 순서를 반영하여 트랜잭션에 커밋 타임스탬프를 할당하여 이를 실행합니다.
Spanner는 외적 일관성이라는 훨씬 강력한 보장을 제공합니다. 즉, 트랜잭션은 커밋 타임스탬프에 반영된 순서로 커밋될 뿐만 아니라 이러한 타임스탬프는 실제 시간과도 일치합니다. 이를 통해 커밋 타임스탬프를 실시간과 비교하여 데이터를 일관되게 전역적으로 정렬된 뷰로 제공할 수 있습니다.
즉, 트랜잭션 Txn1
이 실시간으로 다른 트랜잭션 Txn2
보다 먼저 커밋되면 Txn1
의 커밋 타임스탬프가 Txn2
의 커밋 타임스탬프보다 빨라집니다.
다음 예시를 참조하세요.
이 시나리오에서 타임라인 t
동안 다음이 수행됩니다.
- 트랜잭션
Txn1
은 데이터A
를 읽고A
에 쓰기를 스테이징한 후 성공적으로 커밋됩니다. - 트랜잭션
Txn2
는Txn1
이 시작된 후에 시작됩니다. 데이터B
를 읽은 다음 데이터A
를 읽습니다.
Txn2
가 Txn1이 완료되기 전에 시작되었지만 Txn2
는 Txn1
이 A
에 적용한 변경사항을 관찰합니다. 이는 Txn1
가 A
에 대한 쓰기를 커밋한 후에 Txn2
가 A
를 읽기 때문입니다.
Txn1
과 Txn2
는 실행 시간에 겹칠 수 있지만 커밋 타임스탬프(각각 c1
및 c2
)는 선형 트랜잭션 순서를 적용합니다. 이는 다음을 의미합니다.
Txn1
내의 모든 읽기 및 쓰기는 단일 시점c1
에 발생한 것으로 보입니다.Txn2
내의 모든 읽기 및 쓰기는 단일 시점c2
에 발생한 것으로 보입니다.- 중요한 점은 쓰기가 다른 머신에서 발생하더라도 커밋된 쓰기의 경우
c1
이c2
보다 먼저 발생한다는 것입니다.Txn2
가 읽기만 수행하는 경우c1
은c2
보다 먼저 실행되거나 동시에 실행됩니다.
이러한 강력한 순서는 후속 읽기 작업에서 Txn2
의 효과를 관찰하면 Txn1
의 효과도 관찰한다는 것을 의미합니다. 이 속성은 성공적으로 커밋된 모든 트랜잭션에 대해 적용됩니다.
트랜잭션 실패 시 읽기 및 쓰기 보장
트랜잭션을 실행하기 위한 호출이 실패하면 사용자는 기본 커밋 호출이 실패한 오류에 따라 가질 수 있는 읽기 및 쓰기 보장을 가집니다.
예를 들어, 'Row Not Found(행을 찾을 수 없음)' 또는 'Row Already Exists(행이 이미 있음)'와 같은 오류는 버퍼링된 변형에 일부 오류가 발생했음을 의미합니다. 클라이언트가 업데이트하려고 하는 행이 존재하지 않는 경우를 예로 들 수 있습니다. 이 경우 읽기 일관성이 보장되고 쓰기가 적용되지 않습니다. 그뿐만 아니라, 행이 없으면 읽기 일관성이 보장됩니다.
트랜잭션 실패 시 읽기 및 쓰기 보장
Spanner 트랜잭션이 실패하면 읽기 및 쓰기에 대해 받는 보장은 commit
작업 중에 발생한 특정 오류에 따라 다릅니다.
예를 들어 'Row Not Found(행을 찾을 수 없음)' 또는 'Row Already Exists(행이 이미 있음)'와 같은 오류 메시지는 버퍼링된 변형을 작성하는 중에 문제가 발생했음을 나타냅니다. 예를 들어 클라이언트가 업데이트하려는 행이 존재하지 않는 경우 이러한 상황이 발생할 수 있습니다. 이러한 시나리오에서는 다음과 같이 진행됩니다.
- 일관된 읽기: 트랜잭션 중에 읽은 모든 데이터는 오류가 발생한 지점까지 일관성을 보장합니다.
- 쓰기가 적용되지 않음: 트랜잭션에서 시도한 변형이 데이터베이스에 커밋되지 않습니다.
- 행 일관성: 오류를 트리거한 존재하지 않는(또는 존재하는 상태) 행이 트랜잭션 내에서 수행된 읽기와 일치합니다.
Spanner에서는 동일한 트랜잭션 내의 다른 진행 중인 작업에 영향을 주지 않으면서 언제든지 비동기 읽기 작업을 취소할 수 있습니다. 이 유연성은 상위 수준의 작업이 취소되거나 초기 결과를 기반으로 읽기를 중단하려는 경우에 유용합니다.
하지만 읽기 취소를 요청한다고 해서 즉시 종료된다는 보장은 없습니다. 취소 요청 후에도 읽기 작업은 다음과 같은 작업을 계속할 수 있습니다.
- 완료됨: 취소가 적용되기 전에 읽기가 처리를 완료하고 결과를 반환할 수 있습니다.
- 다른 이유로 실패: 취소와 같은 다른 오류로 인해 읽기가 종료될 수 있습니다.
- 불완전한 결과 반환: 읽기가 부분적인 결과를 반환할 수 있으며, 이 결과는 트랜잭션 커밋 프로세스의 일부로 확인됩니다.
트랜잭션 commit
작업과의 차이점도 주목할 만합니다. commit
을 취소하면 트랜잭션이 이미 커밋되었거나 다른 이유로 실패하지 않는 한 전체 트랜잭션이 중단됩니다.
성능
이 섹션에서는 읽기-쓰기 트랜잭션 성능에 영향을 미치는 문제를 설명합니다.
잠금 동시 실행 제어
Spanner를 사용하면 여러 클라이언트가 동일한 데이터베이스와 동시에 상호작용할 수 있습니다. 이러한 동시 트랜잭션에서 데이터 일관성을 유지하기 위해 Spanner에는 공유 잠금과 배타적 잠금을 모두 사용하는 잠금 메커니즘이 있습니다.
트랜잭션이 읽기 작업을 수행하면 Spanner는 관련 데이터에 대한 공유 읽기 잠금을 획득합니다. 이러한 공유 잠금을 사용하면 다른 동시 읽기 작업이 동일한 데이터에 액세스할 수 있습니다. 이 동시 실행은 트랜잭션이 변경사항을 커밋할 준비가 될 때까지 유지됩니다.
커밋 단계에서 쓰기가 적용되면 트랜잭션은 잠금을 배타적 잠금으로 업그레이드하려고 시도합니다. 이를 위해 다음을 수행합니다.
- 영향을 받는 데이터에 대한 새로운 공유 읽기 잠금 요청을 차단합니다.
- 해당 데이터에 대한 기존의 모든 공유 읽기 잠금이 해제될 때까지 기다립니다.
- 모든 공유 읽기 잠금이 삭제된 후 배타적 잠금을 설정하여 쓰기 기간 동안 데이터에 대한 유일한 액세스 권한을 부여합니다.
잠금에 대한 참고 사항:
- 세분화: Spanner는 행 및 열 수준에서 잠금을 적용합니다. 즉, 트랜잭션
T1
가albumid
행의A
열에 잠금을 설정하고 있는 경우에도 트랜잭션T2
는 충돌 없이 동일한albumid
행의B
열에 동시에 쓸 수 있습니다. - 읽기 없는 쓰기: 읽기 없는 쓰기의 경우 Spanner에 배타적 잠금이 필요하지 않습니다. 대신 작성자 공유 잠금을 사용합니다. 이는 읽기 없이 쓰기의 적용 순서가 커밋 타임스탬프로 결정되므로 여러 쓰기 작업자가 충돌 없이 동일한 항목에 동시에 작업할 수 있기 때문입니다. 배타적 잠금은 트랜잭션이 작성하려는 데이터를 먼저 읽는 경우에만 필요합니다.
- 행 조회를 위한 보조 색인: 읽기-쓰기 트랜잭션 내에서 행 조회를 수행할 때 보조 색인을 사용하면 성능을 크게 개선할 수 있습니다. 보조 색인을 사용하여 스캔된 행을 더 작은 범위로 제한하면 Spanner는 테이블에서 더 적은 행을 잠그므로 특정 범위 외부의 행을 더 많이 동시 수정할 수 있습니다.
- 외부 리소스 독점 액세스: Spanner의 내부 잠금은 Spanner 데이터베이스 자체 내의 데이터 일관성을 위해 설계되었습니다. Spanner 외부 리소스에 대한 독점 액세스를 보장하는 데는 사용하지 마세요. Spanner는 컴퓨팅 리소스 간의 데이터 이동과 같은 내부 시스템 최적화 등 다양한 이유로 트랜잭션을 중단할 수 있습니다. 애플리케이션 코드에서 명시적으로 또는 Spanner JDBC 드라이버와 같은 클라이언트 라이브러리에서 암시적으로 트랜잭션이 재시도되는 경우 커밋이 성공적으로 시도되는 동안에만 잠금이 보장됩니다.
- 잠금 통계: 데이터베이스 내의 잠금 충돌을 진단하고 조사하려면 잠금 통계 검사 도구를 사용하면 됩니다.
교착 상태 감지
여러 트랜잭션이 교착 상태에 빠진 경우, Spanner는 이를 감지하고 트랜잭션 중 하나를 제외한 모든 트랜잭션을 강제로 중단합니다. 다음 시나리오를 생각해 보세요. Txn1
은 레코드 A
에 대한 잠금을 보유하고 레코드 B
에 대한 잠금을 기다리고 있으며, Txn2
는 레코드 B
에 대한 잠금을 보유하고 레코드 A
에 대한 잠금을 기다리고 있습니다. 이 문제를 해결하려면 트랜잭션 중 하나를 중단하여 잠금을 해제하고 다른 트랜잭션이 진행되도록 허용해야 합니다.
Spanner는 교착 상태 감지를 위해 표준 wound-wait 알고리즘을 사용합니다. Spanner는 충돌하는 잠금을 요청하는 각 트랜잭션의 기간을 계속해서 추적합니다. 이를 통해 더 오래된 트랜잭션이 덜 오래된 트랜잭션을 중단할 수 있습니다. 더 오래된 트랜잭션은 읽기, 쿼리 또는 커밋이 먼저 발생한 트랜잭션입니다.
오래된 트랜잭션에 우선순위를 부여함으로써 Spanner는 모든 트랜잭션이 우선순위가 높아질 만큼 충분히 오래되었다는 이유로 결국 잠금을 획득할 수 있는 기회를 보장합니다. 예를 들어 작성자 공유 잠금이 필요한 더 오래된 트랜잭션은 리더 공유 잠금을 보유한 덜 오래된 트랜잭션을 중단할 수 있습니다.
배포 실행
Spanner는 여러 서버에 걸쳐진 데이터에 대해 트랜잭션을 실행할 수 있지만, 이 기능은 단일 서버 트랜잭션에 비해 성능 비용이 발생합니다.
어떤 유형의 트랜잭션이 배포될 수 있을까요? Spanner는 여러 서버에 데이터베이스 행에 대한 책임을 분산할 수 있습니다. 인접한 키를 가지고 있는 동일한 테이블의 두 행과 마찬가지로 어떤 행과 인터리브 처리된 테이블의 해당 행은 일반적으로 동일한 서버에서 처리됩니다. Spanner는 서로 다른 서버의 행에서 트랜잭션을 수행할 수 있습니다. 그러나 일반적으로 같은 위치에 있는 여러 행에 영향을 미치는 트랜잭션은 데이터베이스 전체 또는 큰 테이블 전체에 분산된 여러 행에 영향을 미치는 트랜잭션보다 빠르고 경제적입니다.
Spanner의 가장 효율적인 트랜잭션에는 원자적으로 적용되어야 하는 읽기 및 쓰기만 포함됩니다. 트랜잭션은 모든 읽기 및 쓰기가 키 공간의 동일한 부분에서 데이터에 액세스할 때 가장 빠릅니다.
읽기 전용 트랜잭션
읽기-쓰기 잠금 트랜잭션 외에도 Spanner는 읽기 전용 트랜잭션을 제공합니다.
동일한 타임스탬프에서 읽기를 두 개 이상 실행해야 하는 경우, 읽기 전용 트랜잭션을 사용합니다. Spanner의 단일 읽기 메서드 중 하나를 사용하여 읽기를 표현할 수 있는 경우, 이 단일 읽기 메서드를 대신 사용해야 합니다. 이러한 단일 읽기 호출 사용 시의 성능과 읽기 전용 트랜잭션에서 수행된 단일 읽기의 성능을 비교해야 합니다.
대량의 데이터 읽기를 수행할 경우에는 파티션을 사용하여 데이터 병렬로 읽기를 수행하는 것이 좋습니다.
읽기 전용 트랜잭션은 쓰지 않으므로 잠금을 보유하지 않으며 다른 트랜잭션을 차단하지 않습니다. 읽기 전용 트랜잭션은 트랜잭션 커밋 기록의 일관된 프리픽스를 관찰하므로 애플리케이션이 항상 일관된 데이터를 가져옵니다.
인터페이스
Spanner는 읽기 전용 트랜잭션의 컨텍스트에서 트랜잭션 중단을 위한 재시도와 함께 작업 본문을 실행하기 위한 인터페이스를 제공합니다.
예
다음 예시는 읽기 전용 트랜잭션을 사용하여 동일한 타임스탬프에서 2개의 읽기에 일관된 데이터를 얻는 방법을 보여줍니다.
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
시맨틱스
이 섹션에서는 읽기 전용 트랜잭션의 시맨틱스를 설명합니다.
스냅샷 읽기 전용 트랜잭션
Spanner에서 읽기 전용 트랜잭션이 실행되면 단일 논리 시점에서 모든 읽기를 실행합니다. 즉, 읽기 전용 트랜잭션과 다른 동시 리더 및 작성자 모두 특정 시점에 데이터베이스의 일관된 스냅샷을 확인할 수 있습니다.
이러한 스냅샷 읽기 전용 트랜잭션은 읽기-쓰기 잠금 트랜잭션에 비해 일관된 읽기에 더 간단한 접근 방식을 제공합니다. 이유는 다음과 같습니다.
- 잠금 없음: 읽기 전용 트랜잭션은 잠금을 획득하지 않습니다. 대신 Spanner 타임스탬프를 선택하고 해당 데이터의 이전 버전에 대해 모든 읽기를 실행하여 작동합니다. 잠금을 사용하지 않으므로 동시 실행되는 읽기-쓰기 트랜잭션을 차단하지 않습니다.
- 중단 없음: 이러한 트랜잭션은 절대 중단되지 않습니다. 선택한 읽기 타임스탬프가 가비지 컬렉션된 경우 실패할 수 있지만 Spanner의 기본 가비지 컬렉션 정책은 일반적으로 관대하여 대부분의 애플리케이션에서는 이 문제가 발생하지 않습니다.
- 커밋 또는 롤백 없음: 읽기 전용 트랜잭션에는
sessions.commit
또는sessions.rollback
호출이 필요하지 않으며 실제로는 이러한 호출이 차단됩니다.
스냅샷 트랜잭션을 실행하기 위해 클라이언트는 Spanner에 읽기 타임스탬프를 선택하는 방법을 알려주는 타임스탬프 경계를 정의합니다. 타임스탬프 경계 유형에는 다음이 포함됩니다.
- 강력 읽기: 이 읽기는 읽기가 시작되기 전에 커밋된 모든 트랜잭션의 영향을 확인할 수 있도록 보장합니다. 단일 읽기 내의 모든 행은 일관됩니다. 그러나 강력 읽기는 타임스탬프를 반환하지만 동일한 타임스탬프에서 다시 읽는 것은 반복할 수 없으므로 강력 읽기는 반복할 수 없습니다. 연속된 두 번의 강력 읽기 전용 트랜잭션은 동시 쓰기로 인해 서로 다른 결과를 생성할 수 있습니다. 변경 내역에 대한 쿼리는 이 경계를 사용해야 합니다. 자세한 내용은 TransactionOptions.ReadOnly.strong을 참고하세요.
- 완전 비활성: 이 옵션은 절대 타임스탬프 또는 현재 시간에 비교해 비활성 기간으로 지정된 타임스탬프에서 읽기를 실행합니다. 이로써 해당 타임스탬프까지 전역 트랜잭션 내역의 일관된 프리픽스를 확인하고 읽기 타임스탬프보다 작거나 같은 타임스탬프로 커밋할 수 있는 충돌 트랜잭션을 차단합니다. 제한된 비활성 모드보다 약간 빠르지만 이전 데이터를 반환할 수 있습니다. 자세한 내용은 TransactionOptions.ReadOnly.read_timestamp 및 TransactionOptions.ReadOnly.exact_staleness를 참고하세요.
- 제한된 비활성: Spanner는 사용자 정의 비활성 한도 내에서 최신 타임스탬프를 선택하여 차단하지 않고 사용할 수 있는 가장 가까운 복제본에서 실행할 수 있습니다. 반환된 모든 행은 일관됩니다. 강력 읽기와 마찬가지로 제한된 비활성 상태는 반복할 수 없습니다. 동일한 경계를 적용하더라도 다른 읽기가 다른 타임스탬프에서 실행될 수 있기 때문입니다. 이러한 읽기는 두 단계(타임스탬프 협상 후 읽기)로 작동하며 일반적으로 완전 비활성보다 약간 느리지만 더 최신 결과를 반환하는 경우가 많으며 로컬 복제본에서 실행될 가능성이 더 높습니다. 타임스탬프 협상에는 어떤 행이 읽힐지 미리 알아야 하므로 이 모드는 일회용 읽기 전용 트랜잭션에만 사용할 수 있습니다. 자세한 내용은 TransactionOptions.ReadOnly.max_staleness 및 TransactionOptions.ReadOnly.min_read_timestamp을 참고하세요.
Partitioned DML 트랜잭션
Partitioned DML을 사용하면 트랜잭션 한도에 걸리거나 전체 테이블을 잠그지 않고 대규모 UPDATE
및 DELETE
문을 실행할 수 있습니다. Spanner는 키 공간을 파티션으로 나누고 각 파티션에서 별도의 읽기-쓰기 트랜잭션으로 DML 문을 실행하여 이를 달성합니다.
비Partitioned DML을 사용하려면 코드에서 명시적으로 만드는 읽기-쓰기 트랜잭션 내에서 문을 실행합니다. 자세한 내용은 DML 사용을 참고하세요.
인터페이스
Spanner는 단일 Partitioned DML 문을 실행하기 위한 TransactionOptions.partitionedDml 인터페이스를 제공합니다.
예시
다음 코드 예시에서는 Albums
테이블의 MarketingBudget
열을 업데이트합니다.
C++
ExecutePartitionedDml()
함수를 사용하여 Partitioned DML 문을 실행합니다.
C#
ExecutePartitionedUpdateAsync()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
Go
PartitionedUpdate()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
Java
executePartitionedUpdate()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
Node.js
runPartitionedUpdate()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
PHP
executePartitionedUpdate()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
Python
execute_partitioned_dml()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
Ruby
execute_partitioned_update()
메서드를 사용하여 Partitioned DML 문을 실행합니다.
다음 코드 예시에서는 SingerId
열을 기준으로 Singers
테이블에서 행을 삭제합니다.
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
시맨틱스
이 섹션에서는 Partitioned DML의 시맨틱스를 설명합니다.
Partitioned DML 실행 이해
클라이언트 라이브러리 메서드 또는 Google Cloud CLI 사용 여부에 관계없이 한 번에 Partitioned DML 문 하나만 실행할 수 있습니다.
파티션을 나눈 트랜잭션은 커밋이나 롤백을 지원하지 않습니다. Spanner는 DML 문을 즉시 실행하고 적용합니다. 작업을 취소하거나 작업이 실패하면 Spanner는 실행 중인 파티션을 모두 취소하며, 나머지 파티션을 시작하지 않습니다. 하지만 Spanner는 이미 실행된 파티션을 롤백하지 않습니다.
Partitioned DML 잠금 획득 전략
잠금 경합을 줄이기 위해 Partitioned DML은 WHERE
절과 일치하는 행에 대해서만 읽기 잠금을 획득합니다. 각 파티션에 사용되는 소규모의 독립적인 트랜잭션도 잠금을 더 짧은 시간 동안 보유합니다.
세션 트랜잭션 한도
Spanner의 각 세션에는 한 번에 하나의 활성 트랜잭션이 있을 수 있습니다. 여기에는 내부적으로 트랜잭션을 사용하고 이 한도에 포함되는 독립형 읽기 및 쿼리가 포함됩니다. 트랜잭션이 완료되면 세션을 다음 트랜잭션에 즉시 재사용할 수 있습니다. 트랜잭션마다 새 세션을 만들 필요가 없습니다.
이전 읽기 타임스탬프 및 버전 가비지 컬렉션
Spanner는 버전 가비지 컬렉션을 수행하여 삭제되거나 덮어쓴 데이터를 수집하고 저장소를 확보합니다. 기본적으로 1시간이 지난 데이터는 확보됩니다. Spanner는 구성된 VERSION_RETENTION_PERIOD
보다 오래된 타임스탬프에서 읽기를 수행할 수 없습니다. 기본값이 1시간이지만 최대 1주까지 구성할 수 있습니다. 실행 중에 읽기가 너무 오래되면 실패하고 FAILED_PRECONDITION
오류가 반환됩니다.
변경 내역 쿼리
변경 내역은 전체 데이터베이스, 특정 테이블 또는 데이터베이스 내 정의된 열 집합에서 데이터 수정사항을 모니터링하도록 구성할 수 있는 스키마 객체입니다.
변경 내역을 만들면 Spanner에서 상응하는 SQL 테이블 값 함수(TVF)를 정의합니다. 이 TVF를 사용하여 sessions.executeStreamingSql
메서드로 연결된 변경 내역의 변경 레코드를 쿼리할 수 있습니다. TVF의 이름은 변경 내역의 이름에서 생성되며 항상 READ_
로 시작합니다.
변경 내역 TVF에 대한 모든 쿼리는 강력한 읽기 전용 timestamp_bound
가 있는 일회용 읽기 전용 트랜잭션 내에서 sessions.executeStreamingSql
API를 사용하여 실행해야 합니다. 변경 내역 TVF를 사용하면 기간에 start_timestamp
및 end_timestamp
를 지정할 수 있습니다. 이 강력한 읽기 전용 timestamp_bound
를 사용하여 보관 기간 내의 모든 변경 레코드에 액세스할 수 있습니다. 다른 모든 TransactionOptions
는 변경 내역 쿼리에 유효하지 않습니다.
또한 TransactionOptions.read_only.return_read_timestamp
가 true
로 설정되면 트랜잭션을 설명하는 Transaction
메시지는 유효한 읽기 타임스탬프 대신 특수 값 2^63 - 2
를 반환합니다. 이 특수 값은 삭제해야 하며 이후 쿼리에 사용해서는 안 됩니다.
자세한 내용은 변경 내역 쿼리 워크플로를 참고하세요.
유휴 트랜잭션
대기 중인 읽기 또는 SQL 쿼리가 없고 지난 10초 동안 시작되지 않은 경우 트랜잭션이 유휴 상태로 간주됩니다. Spanner는 유휴 트랜잭션을 중단하여 잠금을 무기한 보유하지 못하도록 할 수 있습니다. 유휴 트랜잭션이 중단되면 커밋이 실패하고 ABORTED
오류가 반환됩니다.
트랜잭션 내에서 SELECT 1
과 같은 작은 쿼리를 주기적으로 실행하면 트랜잭션이 유휴 상태가 되는 것을 방지할 수 있습니다.