並行控制

Spanner 交易提供兩種並行控制模式:悲觀樂觀。並行控制模式的選擇會影響交易處理同步讀取和寫入作業的方式,進而影響效能、延遲和交易中止率。請選擇最符合應用程式效能和一致性需求的模式。

預設行為取決於交易使用的隔離等級

悲觀並行控制

根據預設,Spanner 會使用悲觀並行,並採用可序列化隔離。您也可以搭配可重複讀取隔離層級使用悲觀並行。

可序列化隔離中的悲觀並行

這個模式會假設並行交易可能會爭奪相同資料。在交易中讀取或寫入資料時,系統會主動取得資料的鎖定 。此外,它也會驗證交易中先前取得的鎖定,是否仍保留在後續陳述式中。Spanner 偵測到鎖定衝突時,會使用「受傷等待」演算法解決衝突。

在悲觀並行中,交易會在交易的執行和提交階段取得資料鎖定。

  • 讀取作業:交易讀取資料時,會在執行階段取得共用讀取 (ReaderShared) 鎖定。這些鎖定會保留到交易提交為止。
  • DML 和寫入作業:
    • 執行期間,交易可能會取得資料列存在性的讀取鎖定,以用於 DML 或寫入作業修改的資料。
    • 在提交時,交易會嘗試取得所寫入資料的寫入或專屬鎖定。寫入鎖定會封鎖並行讀取,但可能不會封鎖並行寫入,尤其是在兩者都使用寫入鎖定的情況下。也就是說,多筆交易可以繼續進行修訂,而寫入/寫入衝突會在修訂時使用 wound-wait 演算法解決。所有鎖定都會保留,直到交易提交為止。

可重複讀取隔離層級的悲觀並行

在可重複讀取隔離層級中使用悲觀並行,將寫入作業序列化。在此模式下,讀取作業會使用快照,但專屬鎖定適用於從 FOR UPDATE 查詢或 lock_scanned_ranges=exclusive 提示讀取的資料,以及使用 DML 查詢寫入的資料。

使用可序列化隔離層級的悲觀並行控制的好處

在可序列化隔離層級使用悲觀並行控制的主要優點是,在高度競爭的工作負載中,這有助於交易進展。發生衝突時,Spanner 會優先處理較舊的交易,確保交易最終能完成,並減少重複取消交易的次數。

使用可重複讀取隔離層級的悲觀並行控制的好處

如果採用可重複讀取隔離層級,交易在修訂時仍可能中止,因為查詢 (使用 FOR UPDATE) 或 DML 查詢讀取的資料,在交易修訂前已由並行交易修改。不過,取得鎖定後,系統會防止進一步的並行更新,直到交易提交為止,並將寫入作業序列化。

悲觀並行作業的風險

採用可序列化隔離等級的悲觀並行作業會帶來下列風險:

  • 長時間執行的讀取作業可能會阻斷對延遲時間敏感的寫入作業。
  • 如果交易完成前需要使用者互動,可能會導致鎖定狀態維持很長一段時間,進而封鎖其他作業。

使用可序列化隔離層級的悲觀並行控制用途

悲觀並行適合讀寫和寫寫爭用較高的工作負載。如果交易中止和重試的成本很高,也適合使用這項功能。除非工作負載的鎖定延遲時間過長,或受到鎖定衝突的影響很大,否則請使用這個預設模式。

使用可重複讀取隔離層級的悲觀並行控制用途

如果工作負載需要 FOR UPDATE 子句或 DML 查詢來取得鎖定,請使用悲觀並行控制和可重複讀取。如果您從其他資料庫將工作負載遷移至 Spanner,且這些工作負載會取得這些陳述式的鎖定,這種做法就特別實用。

最佳化並行控制

Spanner 也提供樂觀並行控制。使用可重複讀取隔離等級時,預設模式為樂觀並行控制。您也可以設定可序列化的隔離層級,使用樂觀並行控制。

最佳化並行控制會假設衝突很少發生,即使在讀寫交易中,讀取和查詢作業也會繼續進行,不會取得鎖定。使用 Spanner 的預設可序列化隔離層級時,系統會在提交時驗證讀取作業。這可確保沒有其他並行修訂的交易修改交易先前讀取的資料。如果您使用可重複讀取隔離,系統會在提交時驗證含有 FOR UPDATElock_scanned_ranges=exclusive 提示的讀取作業。如果 Spanner 偵測到衝突,就會中止交易。

樂觀並行控制的運作方式

樂觀並行會改變 Spanner 執行讀取、查詢和提交交易的方式。在讀取階段執行無鎖定作業,並在提交時驗證一致性。

讀取和查詢

讀取和查詢作業不會鎖定。樂觀交易中的所有讀取和查詢作業,都會在單一快照時間戳記執行。Spanner 會在執行第一次讀取或查詢時選擇這個時間戳記。這樣可確保交易中的所有後續讀取和查詢作業,都會看到在第一次讀取或查詢前修訂的寫入作業。

讀取和寫入

如果是包含讀寫作業的樂觀交易,Spanner 會在修訂時執行驗證步驟。只有在未偵測到任何衝突,且符合下列條件時,交易才會成功提交:

  • 沒有任何並行修訂的寫入作業與這項交易讀取的資料衝突,也就是說,在讀取時間戳記之後,但這項交易修訂自身寫入作業之前,沒有任何寫入作業修訂。
  • 自讀取時間戳記以來,結構定義未經過修改。

隔離等級會決定要驗證的讀取作業集。使用可序列化隔離等級時,系統會驗證所有讀取作業。使用可重複讀取隔離層級時,系統會在提交時驗證含有 FOR UPDATElock_scanned_ranges=exclusive 提示的讀取作業。

在高度爭用情況下,樂觀交易可能會反覆中止。相較之下,悲觀交易會允許較舊的交易提交,並重試較新的交易,藉此解決讀寫衝突。

樂觀並行控制的優點

樂觀並行具備下列優點:

  • 讀取作業不會取得鎖定:樂觀交易不會取得讀取作業的鎖定,因此長時間執行的讀取作業不會封鎖延遲時間敏感的寫入作業。
  • 縮短唯讀交易的提交延遲時間:由於樂觀交易中的所有讀取作業都以相同的快照時間戳記為準,因此在執行或提交這些讀取作業時,不需要驗證一致性,可大幅縮短延遲時間。

樂觀並行作業的風險

樂觀並行會帶來風險,特別是在搭配可序列化隔離使用時,若讀寫爭用情況嚴重,請先瞭解這些風險,再對工作負載使用可序列化隔離的樂觀並行控制。

  • 在讀寫爭用程度較高的情況下,樂觀交易的終止率可能會偏高,因為並行寫入可能會使樂觀交易的讀取失效。
  • 如果持續發生高爭用情形,交易可能會重複中止,且永遠不會因交易資源不足而提交。

樂觀並行控制的用途

樂觀並行適用於讀寫爭用較低的交易工作負載。對於可序列化交易,這項功能也適用於可容許交易中止的工作負載。

下列工作負載適合採用樂觀並行控制:

  • 長時間交易的低優先順序工作負載,可容許延遲: 如果長時間讀取或查詢可能會延遲對延遲時間敏感的寫入作業,請使用樂觀並行。這樣可避免讀取鎖定造成的延遲。舉例來說,行動用戶端連線速度緩慢時的交易,或是服務等級協議較低的交易,會為許多資料列或大範圍保留讀取鎖定。
  • 讀取延遲時間敏感的交易工作負載,且讀寫爭用情況較少:多區域設定中,使用樂觀並行來提供區域讀取服務、縮短讀取延遲時間,並避免因熱門分割區的讀取流量暴增而導致生產環境問題。此外,還能提升讀取可用性,避免領導者過度負載或無法使用。
  • 交易工作負載,其中大多數交易都是唯讀: 切換至樂觀並行控制,可減少這些工作負載中常見唯讀交易的提交延遲時間。請確保讀寫爭用率偏低,以免讀寫交易的中止率過高。

對於讀寫衝突頻繁的延遲時間敏感型交易工作負載,請避免使用樂觀並行。

設定並行控制

您可以使用 Spanner 用戶端程式庫、REST 和 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";
}

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode;

/**
 * Shows how to run a Read Write transaction with read lock mode options.
 *
 * Example:
 * ```
 * read_lock_mode($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_lock_mode(string $instanceId, string $databaseId): void
{
    // The read lock mode specified at the client-level will be applied to all
    // RW transactions.
    $spanner = new SpannerClient([
        'readLockMode' => ReadLockMode::OPTIMISTIC
    ]);
    $instance = $spanner->instance($instanceId);
    $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.
    $database->runTransaction(function (Transaction $t) {
        // Read an AlbumTitle.
        $results = $t->execute('SELECT AlbumTitle from Albums WHERE SingerId = 2 and AlbumId = 1');
        foreach ($results as $row) {
            printf('Current Album Title: %s' . PHP_EOL, $row['AlbumTitle']);
        }

        // Update the AlbumTitle.
        $rowCount = $t->executeUpdate('UPDATE Albums SET AlbumTitle = \'A New Title\' WHERE SingerId = 2 and AlbumId = 1');

        // Commit the transaction!
        $t->commit();

        printf('%d record(s) updated.' . PHP_EOL, $rowCount);
    }, [
        'transactionOptions' => [
            'readLockMode' => ReadLockMode::PESSIMISTIC
        ]
    ]);
}

Ruby

require "google/cloud/spanner"

def spanner_read_lock_mode project_id:, instance_id:, database_id:
  # Instantiates a client with read_lock_mode: :OPTIMISTIC
  spanner = Google::Cloud::Spanner.new project: project_id
  client = spanner.client instance_id, database_id, read_lock_mode: :OPTIMISTIC

  # Overrides read_lock_mode to :PESSIMISTIC at transaction level
  client.transaction read_lock_mode: :PESSIMISTIC do |tx|
    results = tx.execute_query "SELECT AlbumTitle FROM Albums WHERE SingerId = 2 AND AlbumId = 1"

    results.rows.each do |row|
      puts "AlbumTitle: #{row[:AlbumTitle]}"
    end

    row_count = tx.execute_update "UPDATE Albums SET AlbumTitle = 'A New Title' WHERE SingerId = 2 AND AlbumId = 1"

    puts "#{row_count} records updated."
  end
end

REST

Spanner TransactionOptions REST API 會在 ReadWrite 訊息中提供 ReadLockMode 列舉,讓您選取 PESSIMISTICOPTIMISTIC 鎖定模式。

RPC

Spanner Transactionoptions RPC API 會在 ReadWrite 訊息中提供 ReadLockMode 列舉,讓您選取 PESSIMISTICOPTIMISTIC 鎖定模式。

駕駛人

您可以使用 Spanner 的驅動程式,在連線層級將 read_lock_mode 設為連線參數,或在交易層級將 SET 設為陳述式選項。如要進一步瞭解各個驅動程式,請參閱「驅動程式總覽」。

後續步驟