同時実行制御

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

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

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

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

シリアル化可能な分離でのペシミスティック コンカレンシー

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

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

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

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

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

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

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

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

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

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

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

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

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

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

反復可能な読み取りの分離を使用した悲観的同時実行のユースケース

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

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

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

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

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

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

読み取りとクエリの場合

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

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

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

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

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

競合が高い場合、楽観的トランザクションが繰り返し中止されることがあります。一方、ペシミスティック トランザクションは、古いトランザクションの 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