同時実行制御

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

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

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

デフォルトでは、Spanner は シリアル化可能な分離でペシミスティック同時実行を使用します。反復可能な読み取りの分離で ペシミスティック同時実行を使用することもできます。

シリアル化可能な分離でのペシミスティック同時実行

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

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

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

反復可能な読み取りの分離でのペシミスティック同時実行

反復可能な読み取りの分離でペシミスティック同時実行を使用して、書き込みをシリアル化します。このモードでは、読み取りオペレーションはスナップショットを使用しますが、 排他ロックFOR UPDATE クエリまたは lock_scanned_ranges=exclusive ヒントから読み取られたデータと、DML クエリで書き込まれたデータに適用されます。

シリアル化可能な分離でのペシミスティック同時実行のメリット

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

反復可能な読み取りの分離でのペシミスティック同時実行のメリット

反復可能な読み取りの分離では、ロックを取得したトランザクションが、FOR UPDATE を使用したクエリの一部として読み取られたデータ、または DML クエリの一部として読み取られたデータが、トランザクションの commit 前に同時実行トランザクションによって変更された場合、commit 時に中止される可能性があります。ただし、ロックを取得すると、トランザクションが commit されるまで、それ以上の同時更新を防ぎ、書き込みをシリアル化します。

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

シリアル化可能な分離でのペシミスティック同時実行には、次のリスクがあります。

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

シリアル化可能な分離でのペシミスティック同時実行のユースケース

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

反復可能な読み取りの分離でのペシミスティック同時実行のユースケース

ロックを取得するために FOR UPDATE 句または DML クエリが必要なワークロードには、反復可能な読み取りでペシミスティック同時実行を使用します。このアプローチは、これらのステートメントのロックを取得する他のデータベースから Spanner に移行されたワークロードに特に役立ちます。

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

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

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

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

オプティミスティック同時実行は、Spanner が読み取り、クエリ、トランザクションの commit を実行する方法を変更します。読み取りフェーズでロックフリー実行を行い、commit 時に整合性を検証します。

読み取りとクエリの場合

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

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

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

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

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

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

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

オプティミスティック同時実行には、次の利点があります。

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

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

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

  • 読み取り / 書き込みの競合が多い場合、同時書き込みによってオプティミスティック トランザクションの読み取りが無効になる可能性があるため、オプティミスティック トランザクションの中止率が高くなる可能性があります。
  • 競合が継続的に発生すると、トランザクションが繰り返し中止され、トランザクションの飢餓状態から 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();
    }
}

C++

void ReadLockModeSetting(std::string const& project_id,
                         std::string const& instance_id,
                         std::string const& database_id) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::Options;
  using ::google::cloud::StatusOr;

  auto db = spanner::Database(project_id, instance_id, database_id);

  // The read lock mode specified at the client-level will be applied
  // to all RW transactions.
  auto options = Options{}.set<spanner::TransactionReadLockModeOption>(
      spanner::Transaction::ReadLockMode::kOptimistic);
  auto client = spanner::Client(spanner::MakeConnection(db, options));

  auto commit = client.Commit(
      [&client](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        // Read an AlbumTitle.
        auto sql = spanner::SqlStatement(
            "SELECT AlbumTitle from Albums WHERE SingerId = @SingerId and "
            "AlbumId = @AlbumId",
            {{"SingerId", spanner::Value(2)}, {"AlbumId", spanner::Value(1)}});
        auto rows = client.ExecuteQuery(txn, std::move(sql));
        for (auto const& row :
             spanner::StreamOf<std::tuple<std::string>>(rows)) {
          if (!row) return row.status();
          std::cout << "Current Album Title: " << std::get<0>(*row) << "\n";
        }

        // Update the title.
        auto update_sql = spanner::SqlStatement(
            "UPDATE Albums "
            "SET AlbumTitle = @AlbumTitle "
            "WHERE SingerId = @SingerId and AlbumId = @AlbumId",
            {{"AlbumTitle", spanner::Value("A New Title")},
             {"SingerId", spanner::Value(2)},
             {"AlbumId", spanner::Value(1)}});
        auto result = client.ExecuteDml(txn, std::move(update_sql));
        if (!result) return result.status();
        std::cout << result->RowsModified() << " record(s) updated.\n";

        return spanner::Mutations{};
      },
      // The read lock mode specified at the transaction-level takes
      // precedence over the read lock mode configured at the client-level.
      // kPessimistic is used here to demonstrate overriding the client-level
      // setting.
      Options{}.set<spanner::TransactionReadLockModeOption>(
          spanner::Transaction::ReadLockMode::kPessimistic));

  if (!commit) throw std::move(commit).status();
  std::cout << "Update was successful [spanner_read_lock_mode]\n";
}

REST

Spanner TransactionOptions REST API の ReadWrite メッセージには ReadLockMode 列挙型があり、 PESSIMISTIC ロックモードまたは OPTIMISTIC ロックモードを選択できます。

RPC

Spanner Transactionoptions RPC API の ReadWrite メッセージには ReadLockMode 列挙型があり、PESSIMISTIC ロックモードまたは OPTIMISTIC ロックモードを選択できます。

ドライバ

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

次のステップ