동시 실행 제어

Spanner 트랜잭션은 비관적낙관적이라는 두 가지 동시성 제어 모드를 제공합니다. 동시성 제어 모드 선택은 트랜잭션이 동시 읽기 및 쓰기를 처리하는 방식에 영향을 미치며 성능, 지연 시간, 트랜잭션 중단 비율에 영향을 미칩니다. 애플리케이션의 성능 및 일관성 요구사항에 가장 적합한 모드를 선택하세요.

기본 동작은 트랜잭션에서 사용하는 격리 수준에 따라 다릅니다.

비관적 동시 실행 제어

기본적으로 Spanner는 직렬화 가능 격리와 함께 비관적 동시 실행을 사용합니다. 이 모드는 동시 트랜잭션이 동일한 데이터를 두고 경합할 수 있다고 가정합니다. 트랜잭션 내에서 데이터를 읽거나 쓸 때 데이터에 잠금을 사전 예방적으로 획득합니다. 또한 트랜잭션에서 이전에 획득한 잠금이 후속 문에서 유지되는지 확인합니다. Spanner는 잠금 충돌을 감지하면 wound-wait 알고리즘을 사용하여 충돌을 해결합니다.

비관적 동시 실행에서 트랜잭션은 트랜잭션의 실행 단계와 커밋 단계 모두에서 데이터에 대한 잠금을 획득합니다.

  • 읽기: 트랜잭션이 데이터를 읽으면 실행 단계에서 공유 읽기 (ReaderShared) 잠금을 획득합니다. 이러한 잠금은 트랜잭션이 커밋될 때까지 유지됩니다.
  • DML 및 쓰기의 경우:
    • 실행 중에 DML 또는 쓰기로 수정된 데이터의 경우 트랜잭션이 행 존재에 대한 읽기 잠금을 획득할 수 있습니다.
    • 커밋 시 트랜잭션은 쓰기 또는 배타적 잠금을 획득하려고 시도합니다. 쓰기 잠금은 동시 읽기를 차단하지만, 특히 둘 다 쓰기 잠금을 사용하는 경우 동시 쓰기는 차단하지 않을 수 있습니다. 즉, 여러 트랜잭션이 커밋으로 진행될 수 있으며 쓰기-쓰기 충돌은 wound-wait 알고리즘을 사용하여 커밋 시간에 해결됩니다. 모든 잠금은 트랜잭션이 커밋될 때까지 유지됩니다.

직렬화 가능 격리를 사용한 비관적 동시 실행의 이점

직렬화 가능한 격리를 사용하여 비관적 동시 실행을 사용하는 주요 이점은 경쟁이 심한 워크로드에서 트랜잭션이 진행되도록 지원한다는 것입니다. Spanner는 충돌이 발생할 때 오래된 트랜잭션에 최신 트랜잭션보다 우선순위를 부여하여 트랜잭션이 결국 완료되도록 하고 반복적으로 트랜잭션을 중단하는 양을 줄입니다.

비관적 동시 실행의 위험

직렬화 가능 격리가 있는 비관적 동시성에는 다음과 같은 위험이 있습니다.

  • 장기 실행 읽기는 지연 시간에 민감한 쓰기를 차단할 수 있습니다.
  • 완료 전에 사용자 상호작용이 포함된 트랜잭션은 잠금이 오랫동안 유지되어 다른 작업을 차단할 수 있습니다.

비관적 동시 실행 사용 사례

비관적 동시성은 읽기-쓰기 및 쓰기-쓰기 경합이 많은 워크로드에 적합합니다. 거래 중단 및 재시도가 비용이 많이 드는 경우에도 적합합니다. 워크로드에 과도하게 긴 잠금 지연이 있거나 잠금 충돌로 인해 심각한 영향을 받는 경우를 제외하고 이 기본 모드를 사용하세요.

최적의 동시 실행 제어

Spanner는 낙관적 동시 실행 제어도 제공합니다. 반복 가능한 읽기 격리를 사용하는 경우 기본 모드는 낙관적 동시 실행 제어입니다. 낙관적 동시성 제어를 사용하도록 직렬화 가능한 격리를 구성할 수도 있습니다.

낙관적 동시 실행 제어는 충돌이 드물다고 가정합니다. 읽기-쓰기 트랜잭션 내에서도 읽기 및 쿼리는 잠금을 획득하지 않고 진행됩니다. Spanner의 기본 직렬화 가능 격리를 사용하면 커밋 시간에 읽기가 검증됩니다. 이렇게 하면 동시에 커밋된 다른 트랜잭션이 트랜잭션에서 이전에 읽은 데이터를 수정하지 않습니다. 반복 가능한 읽기 격리를 사용하는 경우 FOR UPDATE 또는 lock_scanned_ranges=exclusive 힌트가 있는 읽기는 커밋 시간에 검증됩니다. Spanner에서 충돌을 감지하면 트랜잭션을 중단합니다.

낙관적 동시 실행 작동 방식

낙관적 동시 실행은 Spanner가 읽기, 쿼리, 트랜잭션 커밋을 실행하는 방식을 변경합니다. 읽기 단계에서 잠금 없는 실행을 수행하고 커밋 시 일관성을 검증합니다.

읽기 및 쿼리

읽기 및 쿼리는 잠금되지 않습니다. 낙관적 트랜잭션 내의 모든 읽기 및 쿼리는 단일 스냅샷 타임스탬프에서 실행됩니다. Spanner는 첫 번째 읽기 또는 쿼리가 실행될 때 이 타임스탬프를 선택합니다. 이렇게 하면 트랜잭션 내의 모든 후속 읽기 및 쿼리가 첫 번째 읽기 또는 쿼리 전에 커밋된 쓰기를 볼 수 있습니다.

읽기 및 쓰기

읽기 및 쓰기가 있는 낙관적 트랜잭션의 경우 Spanner는 커밋 시간에 유효성 검사 단계를 실행합니다. 충돌이 감지되지 않고 다음 조건이 충족되는 경우에만 트랜잭션이 커밋됩니다.

  • 동시에 커밋된 쓰기가 이 트랜잭션에서 읽은 데이터와 충돌하지 않습니다. 즉, 읽기 타임스탬프 후이지만 이 트랜잭션이 자체 쓰기를 커밋하기 전에 커밋된 쓰기가 없습니다.
  • 읽기 타임스탬프 이후 스키마가 수정되지 않았습니다.

격리 수준에 따라 검증되는 읽기 집합이 결정됩니다. 직렬화 가능한 격리를 사용하면 모든 읽기가 검증됩니다. 반복 가능한 읽기 격리를 사용하면 FOR UPDATE 또는 lock_scanned_ranges=exclusive 힌트가 있는 읽기가 커밋 시간에 검증됩니다.

경합이 심한 경우 낙관적 트랜잭션이 반복적으로 중단될 수 있습니다. 반면 비관적 트랜잭션은 이전 트랜잭션을 커밋하도록 허용하고 최신 트랜잭션을 재시도하여 읽기-쓰기 충돌을 해결합니다.

낙관적 동시 실행의 이점

낙관적 동시성에는 다음과 같은 이점이 있습니다.

  • 읽기는 잠금을 획득하지 않음: 낙관적 트랜잭션은 읽기에 대해 잠금을 획득하지 않으므로 지연 시간에 민감한 쓰기가 장기 실행 읽기에 의해 차단되지 않습니다.
  • 읽기 전용 트랜잭션의 커밋 지연 시간 감소: 낙관적 트랜잭션 내의 모든 읽기는 동일한 스냅샷 타임스탬프를 기반으로 하므로 이러한 읽기의 실행 또는 커밋 중에 일관성을 확인할 필요가 없어 지연 시간이 크게 줄어듭니다.

낙관적 동시 실행의 위험

낙관적 동시 실행은 특히 직렬화 가능한 격리와 함께 사용할 때 읽기-쓰기 경합이 심한 환경에서 위험을 초래합니다. 워크로드에 직렬화 가능한 격리를 사용하여 낙관적 동시 실행 제어를 사용하기 전에 이러한 위험을 이해하세요.

  • 읽기-쓰기 경합이 심한 경우 동시 쓰기로 인해 낙관적 트랜잭션의 읽기가 무효화될 수 있으므로 낙관적 트랜잭션의 중단 비율이 높을 수 있습니다.
  • 지속적으로 경합이 심한 경우 트랜잭션이 반복적으로 중단되고 트랜잭션 기아로 인해 커밋되지 않을 수 있습니다.

최적의 동시 실행 사용 사례

낙관적 동시성은 읽기-쓰기 경합이 낮은 트랜잭션 워크로드에 적합합니다. 직렬화 가능한 트랜잭션의 경우 트랜잭션 중단을 허용할 수 있는 워크로드에도 도움이 됩니다.

다음 워크로드에는 낙관적 동시 실행을 고려하세요.

  • 지연 시간에 민감하지 않고 우선순위가 낮은 워크로드(장기 실행 트랜잭션 포함): 장기 실행 읽기 또는 쿼리로 인해 지연 시간에 민감한 쓰기가 지연될 수 있는 경우 낙관적 동시 실행을 사용합니다. 이렇게 하면 읽기 잠금으로 인한 지연을 방지할 수 있습니다. 예를 들어 연결이 느린 모바일 클라이언트의 트랜잭션이나 여러 행 또는 넓은 범위에 대해 읽기 잠금을 보유하는 SLA가 낮은 트랜잭션이 있습니다.
  • 읽기-쓰기 경합이 적은 읽기 지연 시간에 민감한 트랜잭션 워크로드: 멀티 리전 구성에서 낙관적 동시성 제어를 사용하여 리전별로 읽기를 제공하고, 읽기 지연 시간을 줄이고, 인기 있는 분할에 대한 급격한 읽기 트래픽으로 인한 프로덕션 문제를 방지합니다. 또한 리더 과부하 또는 사용할 수 없는 상태에서 읽기 가용성을 개선합니다.
  • 대부분의 트랜잭션이 읽기 전용인 트랜잭션 워크로드: 낙관적 동시 실행으로 전환하면 이러한 워크로드에서 일반적인 읽기 전용 트랜잭션의 커밋 지연 시간이 줄어듭니다. 읽기-쓰기 트랜잭션의 높은 중단 비율을 방지하기 위해 읽기-쓰기 경합을 낮게 유지합니다.

읽기-쓰기 충돌이 빈번한 지연 시간에 민감한 트랜잭션 워크로드에는 낙관적 동시 실행을 사용하지 마세요.

동시 실행 제어 구성

Spanner 클라이언트 라이브러리, REST, RPC API를 사용하여 읽기-쓰기 트랜잭션의 동시 실행 모드를 지정할 수 있습니다.

클라이언트 라이브러리

자바

static void readLockModeSetting(DatabaseId db) {
  // The read lock mode specified at the client-level will be applied to all
  // RW transactions.
  DefaultReadWriteTransactionOptions transactionOptions =
      DefaultReadWriteTransactionOptions.newBuilder()
          .setReadLockMode(ReadLockMode.OPTIMISTIC)
          .build();
  SpannerOptions options =
      SpannerOptions.newBuilder()
          .setDefaultTransactionOptions(transactionOptions)
          .build();
  Spanner spanner = options.getService();
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbClient
      // The read lock mode specified at the transaction-level takes precedence
      // over the read lock mode configured at the client-level.
      .readWriteTransaction(Options.readLockMode(ReadLockMode.PESSIMISTIC))
      .run(transaction -> {
        // Read an AlbumTitle.
        String selectSql =
            "SELECT AlbumTitle from Albums WHERE SingerId = 1 and AlbumId = 1";
        String title = null;
        try (ResultSet resultSet = transaction.executeQuery(Statement.of(selectSql))) {
          if (resultSet.next()) {
            title = resultSet.getString("AlbumTitle");
          }
        }
        System.out.printf("Current album title: %s\n", title);

        // Update the title.
        String updateSql =
            "UPDATE Albums "
                + "SET AlbumTitle = 'New Album Title' "
                + "WHERE SingerId = 1 and AlbumId = 1";
        long rowCount = transaction.executeUpdate(Statement.of(updateSql));
        System.out.printf("%d record updated.\n", rowCount);
        return null;
      });
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	pb "cloud.google.com/go/spanner/apiv1/spannerpb"
)

// writeWithTransactionUsingReadLockMode sets the ReadLockMode globally
// by using ClientConfig and shows how to override it for a specific
// transaction. ReadLockMode determines the locking strategy used during
// transaction execution.
func writeWithTransactionUsingReadLockMode(w io.Writer, db string) error {
	ctx := context.Background()

	// Client-level configuration: Applies to all read-write transactions
	// for this client. OPTIMISTIC mode avoids locks during reads and
	// verifies changes during the commit phase.
	cfg := spanner.ClientConfig{
		TransactionOptions: spanner.TransactionOptions{
			ReadLockMode: pb.TransactionOptions_ReadWrite_OPTIMISTIC,
		},
	}
	client, err := spanner.NewClientWithConfig(ctx, db, cfg)
	if err != nil {
		return fmt.Errorf("failed to create client: %w", err)
	}
	defer client.Close()

	// Transaction-level options take precedence over client-level
	// configuration. PESSIMISTIC mode is used here to override the
	// client-level setting and ensure immediate locking during reads.
	txnOpts := spanner.TransactionOptions{
		ReadLockMode: pb.TransactionOptions_ReadWrite_PESSIMISTIC,
	}

	_, err = client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		// In PESSIMISTIC mode with SERIALIZABLE isolation, the transaction
		// acquires a shared lock during this read.
		key := spanner.Key{1, 2}
		row, err := txn.ReadRow(ctx, "Albums", key, []string{"AlbumTitle"})
		if err != nil {
			return fmt.Errorf("failed to read album: %w", err)
		}
		var title string
		if err := row.Column(0, &title); err != nil {
			return fmt.Errorf("failed to get album title: %w", err)
		}
		fmt.Fprintf(w, "Current album title: %s\n", title)

		// Update the album title
		stmt := spanner.Statement{
			SQL: `UPDATE Albums
				SET AlbumTitle = @AlbumTitle
				WHERE SingerId = @SingerId AND AlbumId = @AlbumId`,
			Params: map[string]interface{}{
				"SingerId":   1,
				"AlbumId":    2,
				"AlbumTitle": "New Album Title",
			},
		}
		count, err := txn.Update(ctx, stmt)
		if err != nil {
			return fmt.Errorf("failed to update album: %w", err)
		}
		fmt.Fprintf(w, "Updated %d record(s).\n", count)
		return nil
	}, txnOpts)

	if err != nil {
		return fmt.Errorf("transaction failed: %w", err)
	}
	return nil
}

Node.js

// Imports the Google Cloud Spanner client library
const {Spanner, protos} = require('@google-cloud/spanner');
// The read lock mode specified at the client-level will be applied
// to all RW transactions.
const defaultTransactionOptions = {
  readLockMode:
    protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode
      .OPTIMISTIC,
};

// Instantiates a client with defaultTransactionOptions
const spanner = new Spanner({
  projectId: projectId,
  defaultTransactionOptions,
});

function runTransactionWithReadLockMode() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);
  // The read lock mode specified at the request-level takes precedence over
  // the read lock mode configured at the client-level.
  const readLockModeOptionsForTransaction = {
    readLockMode:
      protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode
        .PESSIMISTIC,
  };

  database.runTransaction(
    readLockModeOptionsForTransaction,
    async (err, transaction) => {
      if (err) {
        console.error(err);
        return;
      }
      try {
        const query =
          'SELECT AlbumTitle FROM Albums WHERE SingerId = 2 AND AlbumId = 1';
        const results = await transaction.run(query);
        // Gets first album's title
        const rows = results[0].map(row => row.toJSON());
        const albumTitle = rows[0].AlbumTitle;
        console.log(`previous album title ${albumTitle}`);

        const update =
          "UPDATE Albums SET AlbumTitle = 'New Album Title' WHERE SingerId = 2 AND AlbumId = 1";
        const [rowCount] = await transaction.runUpdate(update);
        console.log(
          `Successfully updated ${rowCount} record in Albums table.`,
        );
        await transaction.commit();
        console.log(
          'Successfully executed read-write transaction with readLockMode option.',
        );
      } catch (err) {
        console.error('ERROR:', err);
        transaction.end();
      } finally {
        // Close the database when finished.
        await database.close();
      }
    },
  );
}
runTransactionWithReadLockMode();

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
from google.cloud.spanner_v1 import TransactionOptions, DefaultTransactionOptions

# The read lock mode specified at the client-level will be applied to all
# RW transactions.
read_lock_mode_options_for_client = TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC

# Create a client that uses Serializable isolation (default) with
# optimistic locking for read-write transactions.
spanner_client = spanner.Client(
    default_transaction_options=DefaultTransactionOptions(
        read_lock_mode=read_lock_mode_options_for_client
    )
)
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

# The read lock mode specified at the request level takes precedence over
# the read lock mode configured at the client level.
read_lock_mode_options_for_transaction = (
    TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC
)

def update_albums_with_read_lock_mode(transaction):
    # Read an AlbumTitle.
    results = transaction.execute_sql(
        "SELECT AlbumTitle from Albums WHERE SingerId = 2 and AlbumId = 1"
    )
    for result in results:
        print("Current Album Title: {}".format(*result))

    # Update the AlbumTitle.
    row_ct = transaction.execute_update(
        "UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 2 and AlbumId = 1"
    )

    print("{} record(s) updated.".format(row_ct))

database.run_in_transaction(
    update_albums_with_read_lock_mode,
    read_lock_mode=read_lock_mode_options_for_transaction
)

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Threading;
using System.Threading.Tasks;

public class ReadLockModeAsyncSample
{
    public async Task ReadLockModeAsync(string projectId, string instanceId, string databaseId)
    {
        // Create client with ReadLockMode.Optimistic.
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId};ReadLockMode=Optimistic";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        // Create transaction options with ReadLockMode.Pessimistic.
        var transactionOptions = SpannerTransactionCreationOptions.ReadWrite
            .WithReadLockMode(ReadLockMode.Pessimistic);

        using var transaction = await connection.BeginTransactionAsync(transactionOptions, null, CancellationToken.None);

        var cmd = connection.CreateSelectCommand("SELECT AlbumTitle FROM Albums WHERE SingerId = 2 AND AlbumId = 1");
        cmd.Transaction = transaction;
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine($"AlbumTitle: {reader.GetFieldValue<string>("AlbumTitle")}");
            }
        }

        var updateCmd = connection.CreateDmlCommand("UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 2 AND AlbumId = 1");
        updateCmd.Transaction = transaction;
        var rowCount = await updateCmd.ExecuteNonQueryAsync();
        Console.WriteLine($"{rowCount} records updated.");

        await transaction.CommitAsync();
    }
}

REST

Spanner TransactionOptions REST API는 PESSIMISTIC 또는 OPTIMISTIC 잠금 모드를 선택할 수 있는 ReadWrite 메시지 내에 ReadLockMode enum을 제공합니다.

RPC

Spanner Transactionoptions RPC API는 ReadWrite 메시지 내에 ReadLockMode enum을 제공하여 PESSIMISTIC 또는 OPTIMISTIC 잠금 모드를 선택할 수 있습니다.

드라이버

Spanner 드라이버를 사용하여 연결 수준에서 read_lock_mode를 연결 매개변수로 설정하거나 트랜잭션 수준에서 SET 문 옵션으로 설정할 수 있습니다. 각 드라이버에 관한 자세한 내용은 드라이버 개요를 참고하세요.

다음 단계