직렬화 가능한 격리에서 SELECT FOR UPDATE 사용

이 페이지에서는 직렬화 가능한 격리 수준에서 FOR UPDATE 절을 사용하는 방법을 설명합니다.

FOR UPDATE 절의 잠금 메커니즘은 반복 가능한 읽기 격리 수준과 직렬화 가능한 격리 수준에서 다르게 동작합니다. 직렬화 가능한 격리 수준에서 SELECT 쿼리로 테이블을 스캔할 때 FOR UPDATE 절을 추가하면 행과 열의 교차 지점(즉, 셀 수준)에서 배타적 잠금이 사용 설정됩니다. 이 잠금은 읽기-쓰기 트랜잭션이 지속되는 동안 유지됩니다. 이 기간 동안 FOR UPDATE 절은 현재 트랜잭션이 완료될 때까지 다른 트랜잭션이 잠긴 셀을 수정하지 못하도록 방지합니다.

FOR UPDATE 절 사용 방법에 대한 자세한 내용은 GoogleSQLPostgreSQL FOR UPDATE 참조 가이드를 참조하세요.

FOR UPDATE 절을 사용하는 이유

격리 수준이 덜 엄격한 데이터베이스에서는 동시 실행 중인 트랜잭션이 데이터를 읽은 후 트랜잭션을 커밋하기 전까지 데이터를 업데이트하지 못하도록 보장하기 위해 FOR UPDATE 절이 필요할 수 있습니다. Spanner는 항상 직렬성을 보장하므로, 트랜잭션 내에서 액세스한 데이터가 커밋 시점에 최신 상태인 경우에만 트랜잭션이 성공적으로 커밋됩니다. 따라서 Spanner에서는 트랜잭션 정확성을 보장하기 위해 FOR UPDATE 절을 사용할 필요가 없습니다.

하지만 여러 트랜잭션이 동시에 동일한 데이터를 읽고 쓰는 등 쓰기 경합이 높은 사용 사례에서는, 동시에 실행되는 트랜잭션으로 인해 중단 횟수가 증가할 수 있습니다. 이는 여러 트랜잭션이 동시에 공유 잠금을 획득한 후 배타적 잠금으로 승격하려고 시도할 때 트랜잭션으로 인해 교착 상태가 발생하기 때문입니다. 교착 상태에서는 각 트랜잭션이 서로가 필요한 리소스를 해제하기를 기다리기 때문에 트랜잭션이 영구적으로 차단됩니다. 진행을 위해 Spanner는 교착 상태를 해결하기 위해 하나를 제외한 모든 트랜잭션을 중단합니다. 자세한 내용은 잠금을 참조하세요.

FOR UPDATE 절을 사용하는 트랜잭션은 미래 배타적 잠금을 획득한 뒤 실행을 진행하며, 다른 트랜잭션은 잠금을 얻을 때까지 대기합니다. 충돌이 발생한 트랜잭션은 한 번에 하나씩만 실행할 수 있으므로 Spanner가 처리량을 제한할 수는 있지만, Spanner가 하나의 트랜잭션만 진행하므로 동시에 여러 트랜잭션을 중단하고 재시도하는 데 소요되는 시간을 줄일 수 있습니다.

따라서 동시 쓰기 요청 시 발생하는 트랜잭션 중단 횟수를 줄이는 것이 중요하다면, FOR UPDATE 절을 사용하여 전체 중단 수를 줄이고 워크로드 실행 효율을 높일 수 있습니다.

LOCK_SCANNED_RANGES 힌트와의 비교

FOR UPDATE 절은 LOCK_SCANNED_RANGES=exclusive 힌트와 유사한 기능을 수행합니다.

하지만 다음과 같은 두 가지 주된 차이점이 있습니다.

  • LOCK_SCANNED_RANGES 힌트를 사용하면 트랜잭션은 전체 문에 대해 스캔된 범위에 대한 배타적 잠금을 획득합니다. 서브 쿼리에서 배타적 잠금을 획득할 수 없습니다. 잠금 힌트를 사용하면 필요 이상으로 많은 잠금을 획득하여 워크로드의 잠금 경합에 기여할 수 있습니다. 다음 예시는 잠금 힌트를 사용하는 방법을 보여줍니다.

    @{lock_scanned_ranges=exclusive}
    SELECT s.SingerId, s.FullName FROM Singers AS s
    JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
    AS a ON a.SingerId = s.SingerId;
    

    반면에 다음 예시와 같이 서브 쿼리에서 FOR UPDATE 절을 사용할 수 있습니다.

    SELECT s.SingerId, s.FullName FROM Singers AS s
    JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
    FOR UPDATE AS a ON a.SingerId = s.SingerId;
    
  • LOCK_SCANNED_RANGES 힌트는 DML 문에서 사용할 수 있지만 FOR UPDATE 절은 SELECT 문에서만 사용할 수 있습니다.

잠금 시맨틱스

동시 쓰기 요청과 교착 상태로 인한 트랜잭션 중단 비용을 줄이기 위해 Spanner는 가능한 경우 셀 수준에서 데이터를 잠급니다. 셀 수준은 테이블 내에서 가장 세밀한 데이터 단위로, 행과 열이 교차하는 지점의 데이터 포인트를 의미합니다. FOR UPDATE 절을 사용할 경우, Spanner는 SELECT 쿼리에서 스캔한 특정 셀을 잠급니다.

다음 예시에서 SingerId = 1AlbumId = 1 행의 MarketingBudget 셀은 Albums 테이블에서 배타적으로 잠겨 있으므로 이 트랜잭션이 커밋되거나 롤백될 때까지 동시 트랜잭션이 해당 셀을 수정할 수 없습니다. 하지만 동일한 행의 AlbumTitle 셀은 여전히 다른 동시 트랜잭션에서 수정할 수 있습니다.

SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1
FOR UPDATE;

동시 트랜잭션이 잠긴 데이터를 읽을 때 차단될 수 있음

한 트랜잭션이 스캔된 범위에 대해 배타적 잠금을 획득한 경우, 다른 동시 트랜잭션은 해당 데이터를 읽을 때 차단될 수 있습니다. Spanner는 직렬성을 보장하므로, 트랜잭션이 실행되는 동안 다른 트랜잭션에 의해 변경되지 않았음이 보장되는 경우에만 데이터를 읽을 수 있습니다. 이미 잠긴 데이터를 읽으려는 동시 트랜잭션은 잠금을 보유한 트랜잭션이 커밋되거나 롤백되거나 시간 초과될 때까지 대기해야 할 수 있습니다.

다음 예시에서 Transaction 11 <= AlbumId < 5 범위의 MarketingBudget 셀을 잠급니다.

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

AlbumId = 1에 대한 MarketingBudget을 읽으려고 시도하는 Transaction 2Transaction 1이 커밋되거나 롤백될 때까지 차단됩니다.

-- Transaction 2
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1;

-- Blocked by Transaction 1

마찬가지로 FOR UPDATE 절로 스캔된 범위를 잠그려는 트랜잭션은 중첩된 범위를 잠근 다른 동시 트랜잭션에 의해 차단됩니다.

다음 예시에서는 Transaction 1Transaction 3와 겹치는 스캔 범위인 3 <= AlbumId < 5에 대해 MarketingBudget 셀을 잠갔으므로 Transaction 3도 차단됩니다.

-- Transaction 3
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 3 and AlbumId < 10
FOR UPDATE;

-- Blocked by Transaction 1

색인 읽기

스캔된 범위를 잠그는 쿼리가 기본 테이블의 행을 잠그는 경우 동시 읽기가 차단되지 않을 수 있지만, 동시 트랜잭션은 인덱스에서 읽습니다.

다음 Transaction 1SingerId = 1에 대해 SingerIdSingerInfo 셀을 잠급니다.

-- Transaction 1
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = 1
FOR UPDATE;

읽기 전용 Transaction 2는 색인 테이블을 쿼리하므로 Transaction 1에서 획득한 잠금에 의해 차단되지 않습니다.

-- Transaction 2
SELECT SingerId FROM Singers;

동시 트랜잭션은 이미 잠긴 데이터에 대한 DML 작업을 차단하지 않음

한 트랜잭션이 배타적 잠금 힌트를 사용하여 셀 범위에 잠금을 획득한 경우, 다른 동시 트랜잭션이 잠긴 셀에서 데이터를 먼저 읽지 않고 쓰기 작업을 수행하려고 시도하면 해당 트랜잭션은 계속 진행될 수 있습니다. 트랜잭션은 잠금을 보유한 트랜잭션이 커밋되거나 롤백될 때까지 커밋에서 차단됩니다.

다음 Transaction 11 <= AlbumId < 5에 대해 MarketingBudget 셀을 잠급니다.

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

Transaction 2Albums 테이블을 업데이트하려고 시도하면 Transaction 1이 커밋되거나 롤백될 때까지 업데이트가 차단됩니다.

-- Transaction 2
UPDATE Albums
SET MarketingBudget = 200000
WHERE SingerId = 1 and AlbumId = 1;

> Query OK, 1 rows affected

COMMIT;

-- Blocked by Transaction 1

스캔된 범위가 잠기면 기존 행과 간격이 잠김

한 트랜잭션이 스캔된 범위에 대해 배타적 잠금을 획득한 경우, 다른 동시 트랜잭션은 해당 범위 내의 빈 공간에 데이터를 삽입할 수 없습니다.

다음 Transaction 11 <= AlbumId < 10에 대해 MarketingBudget 셀을 잠급니다.

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 10
FOR UPDATE;

Transaction 2가 아직 존재하지 않는 AlbumId = 9에 대한 행을 삽입하려고 하면 Transaction 1이 커밋하거나 롤백할 때까지 삽입이 차단됩니다.

-- Transaction 2
INSERT INTO Albums (SingerId, AlbumId, AlbumTitle, MarketingBudget)
VALUES (1, 9, "Hello hello!", 10000);

> Query OK, 1 rows affected

COMMIT;

-- Blocked by Transaction 1

잠금 획득 시 주의사항

앞서 설명한 잠금 시맨틱스는 일반적인 지침일 뿐이며, Spanner가 FOR UPDATE 절을 사용하는 트랜잭션을 실행할 때 잠금이 정확히 어떤 방식으로 획득되는지를 보장하지는 않습니다. Spanner의 쿼리 최적화 메커니즘에 따라 획득되는 잠금의 범위가 달라질 수도 있습니다. 이 절은 현재 트랜잭션이 완료될 때까지 다른 트랜잭션이 잠긴 셀을 수정하지 못하도록 합니다.

쿼리 구문

이 섹션에서는 FOR UPDATE 절을 사용할 때의 쿼리 구문에 대한 지침을 제공합니다.

가장 일반적인 사용 방식은 최상위 SELECT 문에서 사용하는 것입니다. 예를 들면 다음과 같습니다.

SELECT SingerId, SingerInfo
FROM Singers WHERE SingerID = 5
FOR UPDATE;

이 샘플은 SELECT 문에서 FOR UPDATE 절을 사용하여 WHERE SingerID = 5SingerIdSingerInfo 셀을 배타적으로 잠그는 방법을 보여줍니다.

WITH 문에서의 사용

WITH 문에 대한 외부 수준 쿼리에서 FOR UPDATE를 지정하면 FOR UPDATE 절은 WITH 문에 대한 잠금을 획득하지 않습니다.

다음 쿼리에서는 잠금 의도가 공통 테이블 표현식(CTE) 쿼리로 전파되지 않으므로 Singers 테이블에서 잠금을 획득하지 않습니다.

WITH s AS (SELECT SingerId, SingerInfo FROM Singers WHERE SingerID > 5)
SELECT * FROM s
FOR UPDATE;

FOR UPDATE 절을 CTE 쿼리 내부에 지정하면, CTE 쿼리에서 스캔된 범위에 잠금이 걸립니다.

다음 예시에서는 SingerId > 5가 잠겨 있는 행의 SingerIdSingerInfo 셀입니다.

WITH s AS
  (SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5 FOR UPDATE)
SELECT * FROM s;

서브 쿼리에서의 사용

하나 이상의 서브 쿼리가 있는 외부 수준 쿼리에서 FOR UPDATE 절을 사용할 수 있습니다. 잠금은 최상위 쿼리와 서브 쿼리 내에서 획득됩니다(표현식 서브 쿼리 제외).

다음 쿼리는 SingerId > 5.에 대해 SingerIdSingerInfo 셀을 잠급니다.

(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5) AS t
FOR UPDATE;

다음 쿼리는 표현식 서브 쿼리 내에 있기 때문에 Albums 테이블의 셀에는 잠금이 걸리지 않습니다. 하지만 표현식 서브 쿼리에서 반환된 행의 SingerIdSingerInfo 셀에는 잠금이 걸립니다.

SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
FOR UPDATE;

뷰를 쿼리할 때의 사용

다음 예시와 같이 FOR UPDATE 절을 사용하여 뷰를 쿼리할 수 있습니다.

CREATE VIEW SingerBio AS SELECT SingerId, FullName, SingerInfo FROM Singers;

SELECT * FROM SingerBio WHERE SingerId = 5 FOR UPDATE;

뷰를 정의할 때 FOR UPDATE 절을 사용할 수 없습니다.

지원되지 않는 사용 사례

다음 FOR UPDATE 사용 사례는 지원되지 않습니다.

  • Spanner 외부 코드 실행을 위한 상호 배제 메커니즘으로 사용: Spanner의 잠금 기능을 Spanner 외부 리소스에 대한 배타적 액세스를 보장하기 위한 용도로 사용하지 마세요. 트랜잭션이 재시도되는 경우(애플리케이션 코드에서 명시적으로 또는 Spanner JDBC 드라이버와 같은 클라이언트 코드에서 암묵적으로) Spanner가 트랜잭션을 중단시킬 수 있습니다. 잠금은 커밋된 시도 동안에만 유지된다는 점이 보장됩니다.
  • LOCK_SCANNED_RANGES 힌트와 함께 사용하는 경우: FOR UPDATE 절과 LOCK_SCANNED_RANGES 힌트를 동일한 쿼리에서 동시에 사용할 수 없습니다. 이 둘을 함께 사용하면 Spanner에서 오류가 발생합니다.
  • 전체 텍스트 검색 쿼리에서 사용: 전체 텍스트 검색 색인을 사용하는 쿼리에서는 FOR UPDATE 절을 사용할 수 없습니다.
  • 읽기 전용 트랜잭션에서 사용: FOR UPDATE 절은 읽기-쓰기 트랜잭션 내에서 실행되는 쿼리에서만 유효합니다.
  • DDL 문 내에서 사용: 나중에 실행하기 위해 저장된 DDL 문 내의 쿼리에서는 FOR UPDATE 절을 사용할 수 없습니다. 예를 들어 뷰를 정의할 때 FOR UPDATE 절을 사용할 수 없습니다. 잠금이 필요한 경우 뷰를 정의할 때가 아니라 뷰를 쿼리할 때 FOR UPDATE 절을 지정할 수 있습니다.

다음 단계