同時実行制御

Spanner トランザクションには、ペシミスティックオプティミスティックの 2 つの同時実行制御モードがあります。同時実行制御モードの選択は、トランザクションが同時読み取りと書き込みを処理する方法に影響し、パフォーマンス、レイテンシ、トランザクションの中止率に影響します。アプリケーションのパフォーマンスと整合性の要件に最適なモードを選択します。

デフォルトの動作は、トランザクションで使用する分離レベルによって異なります。

ペシミスティック同時実行制御

デフォルトでは、Spanner はシリアル化可能な分離による悲観的同時実行を使用します。このモードでは、同時実行トランザクションが同じデータを競合する可能性があることを前提としています。トランザクション内でデータが読み取りまたは書き込みされると、データに対して事前にロックを取得します。また、トランザクションの早い段階で取得されたロックが、後のステートメントでも保持されていることを確認します。Spanner は、ロックの競合を検出すると、wound-wait アルゴリズムを使用して競合を解決します。

ペシミスティック同時実行では、トランザクションの実行フェーズと commit フェーズの両方で、トランザクションがデータに対するロックを取得します。

  • 読み取りの場合: トランザクションがデータを読み取ると、実行フェーズで共有読み取り(ReaderShared)ロックを取得します。これらのロックは、トランザクションが commit されるまで保持されます。
  • DML と書き込みの場合:
    • 実行中、DML または書き込みによって変更されたデータに対して、トランザクションが行の存在に関する読み取りロックを取得することがあります。
    • commit 時に、トランザクションは書き込まれたデータに対して書き込みロックまたは排他ロックを取得しようとします。書き込みロックは同時読み取りをブロックしますが、特に両方が書き込みロックを使用している場合、同時書き込みをブロックしないことがあります。つまり、複数のトランザクションを commit に進めることができ、書き込み-書き込みの競合は commit 時に wound-wait アルゴリズムを使用して解決されます。すべてのロックは、トランザクションが commit されるまで保持されます。

シリアル化可能な分離を使用したペシミスティック コンカレンシーのメリット

シリアル化可能な分離でペシミスティック同時実行を使用する主なメリットは、競合の多いワークロードでトランザクションの進行を支援することです。Spanner は、競合が発生した場合、新しいトランザクションよりも古いトランザクションを優先します。これにより、トランザクションが最終的に完了し、トランザクションの繰り返し中止の回数が減ります。

ペシミスティック同時実行のリスク

シリアル化可能な分離を使用したペシミスティック コンカレンシーには、次のリスクがあります。

  • 長時間実行される読み取りは、レイテンシの影響を受けやすい書き込みをブロックする可能性があります。
  • 完了前にユーザー インタラクションを伴うトランザクションでは、ロックが長時間保持され、他のオペレーションがブロックされる可能性があります。

ペシミスティック同時実行のユースケース

ペシミスティック同時実行は、読み取り / 書き込み競合と書き込み / 書き込み競合が多いワークロードに適しています。トランザクションの中止と再試行にコストがかかる場合にも適しています。ワークロードに過度の長時間ロック遅延がある場合や、ロック競合の影響を大きく受ける場合を除き、このデフォルト モードを使用します。

オプティミスティック同時実行制御

Spanner は、オプティミスティック同時実行制御も提供します。反復可能な読み取り分離を使用する場合、デフォルトのモードはオプティミスティック同時実行制御です。楽観的同時実行制御を使用するようにシリアル化可能な分離を構成することもできます。

オプティミスティック同時実行制御では、競合はまれであると想定しています。読み取り / 書き込みトランザクション内であっても、読み取りとクエリはロックを取得せずに続行されます。Spanner のデフォルトのシリアル化可能な分離では、読み取りは commit 時に検証されます。これにより、同時に commit された他のトランザクションが、トランザクションによって以前に読み取られたデータを変更しないことが保証されます。反復可能な読み取りの分離を使用する場合、FOR UPDATE ヒントまたは lock_scanned_ranges=exclusive ヒントのいずれかを含む読み取りは、コミット時に検証されます。Spanner が競合を検出すると、トランザクションが中止されます。

オプティミスティック同時実行の仕組み

楽観的同時実行により、Spanner が読み取り、クエリ、トランザクションの commit を実行する方法が変わります。読み取りフェーズでロックフリー実行を行い、コミット時に整合性を検証します。

読み取りとクエリの場合

読み取りとクエリはロックフリーです。楽観的トランザクション内のすべての読み取りとクエリは、単一のスナップショット タイムスタンプで実行されます。Spanner は、最初の読み取りまたはクエリが実行されるときにこのタイムスタンプを選択します。これにより、トランザクション内の後続の読み取りとクエリで、最初の読み取りまたはクエリの前に commit された書き込みがすべて認識されます。

読み取りと書き込みの場合

読み取りと書き込みを含む楽観的トランザクションの場合、Spanner は commit 時に検証ステップを実行します。競合が検出されず、次の条件が満たされている場合にのみ、トランザクションは正常にコミットされます。

  • 同時に commit された書き込みが、このトランザクションで読み取られたデータと競合しない。つまり、読み取りタイムスタンプの後、このトランザクションが独自の書き込みを commit する前に、書き込みが commit されていない。
  • 読み取りタイムスタンプ以降、スキーマが変更されていない。

分離レベルによって、検証される読み取りのセットが決まります。シリアル化可能な分離では、すべての読み取りが検証されます。反復可能な読み取りの分離では、FOR UPDATE または lock_scanned_ranges=exclusive のヒントを含む読み取りは、コミット時に検証されます。

競合が多い場合、楽観的トランザクションが繰り返し中止されることがあります。一方、ペシミスティック トランザクションは、古いトランザクションの commit を許可し、新しいトランザクションを再試行することで、読み取り / 書き込みの競合を解決します。

オプティミスティック同時実行のメリット

楽観的同時実行には、次の利点があります。

  • 読み取りはロックを取得しない: 楽観的トランザクションは読み取りのロックを取得しないため、長時間実行される読み取りはレイテンシの影響を受けやすい書き込みをブロックしません。
  • 読み取り専用トランザクションの commit レイテンシの短縮: 楽観的トランザクション内のすべての読み取りは同じスナップショット タイムスタンプに基づいているため、これらの読み取りの実行時または commit 時に一貫性を検証する必要がなく、レイテンシが大幅に短縮されます。

オプティミスティック同時実行のリスク

楽観的同時実行にはリスクがあります。特に、シリアル化可能な分離で使用すると、読み取り / 書き込みの競合が激しい場合にリスクが高くなります。ワークロードにシリアル化可能な分離でオプティミスティック同時実行制御を使用する前に、これらのリスクを理解してください。

  • 読み取り / 書き込みの競合が多い場合、同時書き込みによって楽観的トランザクションの読み取りが無効になる可能性があるため、楽観的トランザクションの中止率が高くなる可能性があります。
  • 競合が継続的に発生すると、トランザクションが繰り返し中止され、トランザクションの飢餓状態からコミットされない可能性があります。

オプティミスティック同時実行のユースケース

楽観的同時実行は、読み取り / 書き込みの競合が少ないトランザクション ワークロードに適しています。シリアル化可能なトランザクションの場合、トランザクションの中止を許容できるワークロードにもメリットがあります。

次のワークロードでは、オプティミスティック同時実行を検討してください。

  • 長時間実行されるトランザクションを含む、低優先度でレイテンシ許容度の高いワークロード: 長時間実行される読み取りまたはクエリによってレイテンシの影響を受けやすい書き込みが遅延する可能性がある場合は、楽観的同時実行制御を使用します。これにより、読み取りロックによる遅延を回避できます。たとえば、接続が遅いモバイル クライアントのトランザクションや、多数の行または大きな範囲の読み取りロックを保持している低 SLA トランザクションなどです。
  • 読み取りレイテンシに敏感なトランザクション ワークロードで読み取り / 書き込みの競合が少ない場合: マルチリージョン構成では、楽観的同時実行制御を使用して読み取りをリージョンごとに処理し、読み取りレイテンシを短縮し、ホット スプリットへの読み取りトラフィックの急増による本番環境の問題を回避します。また、リーダーの過負荷や使用不可時の読み取り可用性も向上します。
  • ほとんどのトランザクションが読み取り専用のトランザクション ワークロード: オプティミスティック同時実行制御に切り替えると、これらのワークロードの一般的な読み取り専用トランザクションの commit レイテンシが短縮されます。読み取り / 書き込みの競合を減らし、読み取り / 書き込みトランザクションの中止率が高くならないようにします。

読み取り / 書き込みの競合が頻繁に発生するレイテンシの影響を受けやすいトランザクション ワークロードには、オプティミスティック同時実行制御を使用しないでください。

同時実行制御を構成する

Spanner クライアント ライブラリ、REST API、RPC API を使用して、読み取り / 書き込みトランザクションの同時実行モードを指定できます。

クライアント ライブラリ

Java

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 は、ReadWrite メッセージ内に ReadLockMode 列挙型を提供します。これにより、PESSIMISTIC ロックモードまたは OPTIMISTIC ロックモードを選択できます。

RPC

Spanner の Transactionoptions RPC API は、ReadWrite メッセージ内に ReadLockMode 列挙型を提供します。これにより、PESSIMISTIC ロックモードまたは OPTIMISTIC ロックモードを選択できます。

ドライバ

Spanner のドライバを使用すると、接続レベルで read_lock_mode を接続パラメータとして設定するか、トランザクション レベルで SET ステートメント オプションとして設定できます。各ドライバの詳細については、ドライバの概要をご覧ください。

次のステップ