Gleichzeitigkeitserkennung

Spanner-Transaktionen bieten zwei Modi für die Parallelitätssteuerung: pessimistisch und optimistisch. Die Wahl des Modus für die Nebenläufigkeitssteuerung wirkt sich darauf aus, wie Transaktionen gleichzeitige Lese- und Schreibvorgänge verarbeiten. Dies beeinflusst Leistung, Latenz und Transaktionsabbrüche. Wählen Sie den Modus aus, der den Leistungs- und Konsistenzanforderungen Ihrer Anwendung am besten entspricht.

Das Standardverhalten hängt von der Isolationsstufe ab, die für Ihre Transaktion verwendet wird:

Pessimistische Nebenläufigkeitserkennung

Standardmäßig verwendet Spanner pessimistische Parallelität mit Isolation zur Serialisierbarkeit. In diesem Modus wird davon ausgegangen, dass konkurrierende Transaktionen um dieselben Daten konkurrieren können. Es werden Sperren proaktiv für Daten abgerufen, wenn diese innerhalb einer Transaktion gelesen oder geschrieben werden. Außerdem wird geprüft, ob Sperren, die früher in der Transaktion erworben wurden, in späteren Anweisungen weiterhin gehalten werden. Wenn Spanner einen Sperrkonflikt erkennt, wird der Konflikt mit dem Wound-Wait-Algorithmus behoben.

Bei der pessimistischen Nebenläufigkeit werden während der Ausführungs- und Commit-Phase der Transaktion Sperren für Daten abgerufen.

  • Lesevorgänge:Wenn eine Transaktion Daten liest, wird während der Ausführungsphase eine gemeinsame Lesesperre (ReaderShared) abgerufen. Diese Sperren werden bis zum Commit der Transaktion beibehalten.
  • Für DML und Schreibvorgänge:
    • Während der Ausführung kann die Transaktion für Daten, die durch DML oder Schreibvorgänge geändert werden, Lesesperren für die Existenz von Zeilen erwerben.
    • Beim Commit versucht die Transaktion, Schreib- oder exklusive Sperren für die geschriebenen Daten zu erwerben. Schreibsperren blockieren gleichzeitige Lesevorgänge, aber möglicherweise nicht gleichzeitige Schreibvorgänge, insbesondere wenn beide Schreibsperren verwenden. Das bedeutet, dass mehrere Transaktionen committet werden können und Schreib-/Schreibkonflikte zur Commit-Zeit mit dem Wound-Wait-Algorithmus behoben werden. Alle Sperren werden bis zum Commit der Transaktion beibehalten.

Vorteile der pessimistischen Parallelität mit serialisierbarer Isolation

Der Hauptvorteil der Verwendung von pessimistischer Nebenläufigkeit mit serialisierbarer Isolation besteht darin, dass Transaktionen bei stark umkämpften Arbeitslasten besser vorankommen. Bei Konflikten priorisiert Spanner ältere Transaktionen gegenüber neueren. So wird dafür gesorgt, dass Transaktionen schließlich abgeschlossen werden, und die Anzahl der wiederholten Abbrüche von Transaktionen wird reduziert.

Risiken der pessimistischen Gleichzeitigkeitserkennung

Die pessimistische Parallelität mit serialisierbarer Isolation birgt die folgenden Risiken:

  • Lange Lesevorgänge können latenzempfindliche Schreibvorgänge blockieren.
  • Bei Transaktionen, die vor dem Abschluss eine Nutzerinteraktion erfordern, können Sperren lange aufrechterhalten werden, was möglicherweise andere Vorgänge blockiert.

Anwendungsfälle für pessimistische Parallelität

Die pessimistische Nebenläufigkeit eignet sich für Arbeitslasten mit hohem Lese-/Schreib- und Schreib-/Schreibkonflikt. Das ist auch dann sinnvoll, wenn Transaktionsabbrüche und ‑wiederholungen kostspielig sind. Verwenden Sie diesen Standardmodus, es sei denn, Ihre Arbeitslast weist übermäßig lange Sperrverzögerungen auf oder ist erheblich von Sperrkonflikten betroffen.

Optimistische Nebenläufigkeitserkennung

Spanner bietet auch eine optimistische Nebenläufigkeitserkennung. Wenn Sie die Isolierung „Wiederholbares Lesen“ verwenden, ist der Standardmodus die optimistische Gleichzeitigkeitserkennung. Sie können die serialisierbare Isolation auch so konfigurieren, dass die optimistische Parallelitätssteuerung verwendet wird.

Bei der optimistischen Nebenläufigkeitserkennung wird davon ausgegangen, dass Konflikte selten sind. Lese- und Abfragevorgänge werden auch innerhalb einer Lese-Schreib-Transaktion ohne Sperren ausgeführt. Bei der standardmäßigen serialisierbaren Isolation von Spanner werden Lesevorgänge zum Zeitpunkt des Commits validiert. So wird sichergestellt, dass keine andere gleichzeitig übergebene Transaktion die Daten geändert hat, die zuvor von der Transaktion gelesen wurden. Wenn Sie die Isolation wiederholbarer Lesevorgänge verwenden, werden Lesevorgänge mit dem Hinweis FOR UPDATE oder lock_scanned_ranges=exclusive zum Zeitpunkt des Commits validiert. Wenn Spanner einen Konflikt erkennt, wird die Transaktion abgebrochen.

Funktionsweise der optimistischen Gleichzeitigkeitserkennung

Die optimistische Nebenläufigkeit ändert die Art und Weise, wie Spanner Lese- und Abfragevorgänge ausführt und Transaktionen committet. Sie führt die Ausführung während der Lesevorgangsphase ohne Sperren durch und validiert die Konsistenz beim Commit.

Für Lese- und Abfragevorgänge

Lese- und Abfragevorgänge sind nicht blockierend. Alle Lese- und Abfragevorgänge innerhalb einer optimistischen Transaktion werden zu einem einzelnen Snapshot-Zeitstempel ausgeführt. Spanner wählt diesen Zeitstempel aus, wenn der erste Lese- oder Abfragevorgang ausgeführt wird. So wird sichergestellt, dass bei allen nachfolgenden Lese- und Abfragevorgängen innerhalb der Transaktion Schreibvorgänge berücksichtigt werden, die vor dem ersten Lese- oder Abfragevorgang durchgeführt wurden.

Für Lese- und Schreibvorgänge

Bei einer optimistischen Transaktion mit Lese- und Schreibvorgängen führt Spanner zum Zeitpunkt des Commits einen Validierungsschritt aus. Die Transaktion wird nur dann erfolgreich übernommen, wenn keine Konflikte erkannt werden und die folgenden Bedingungen erfüllt sind:

  • Es gibt keine gleichzeitig ausgeführten Commits, die mit den von dieser Transaktion gelesenen Daten in Konflikt stehen. Das heißt, es wurden keine Schreibvorgänge nach dem Zeitstempel des Lesevorgangs, aber vor dem Commit der eigenen Schreibvorgänge dieser Transaktion ausgeführt.
  • Das Schema wurde seit dem Lesezeitstempel nicht geändert.

Die Isolationsebene bestimmt die Menge der Lesevorgänge, die validiert werden. Bei der serialisierbaren Isolation werden alle Lesevorgänge validiert. Bei der Isolation wiederholbarer Lesevorgänge werden Lesevorgänge mit dem Hinweis FOR UPDATE oder lock_scanned_ranges=exclusive zum Zeitpunkt des Commits validiert.

Bei hoher Konfliktwahrscheinlichkeit können optimistische Transaktionen wiederholt abgebrochen werden. Bei pessimistischen Transaktionen werden Lese-/Schreibkonflikte dagegen dadurch behoben, dass die ältere Transaktion mit Commit bestätigt wird und die neuere Transaktion wiederholt wird.

Vorteile der optimistischen Gleichzeitigkeitserkennung

Die optimistische Parallelität bietet folgende Vorteile:

  • Lesevorgänge erhalten keine Sperren: Optimistische Transaktionen erhalten keine Sperren für Lesevorgänge. Daher blockieren lang andauernde Lesevorgänge keine latenzsensitiven Schreibvorgänge.
  • Geringere Commit-Latenz für schreibgeschützte Transaktionen: Da alle Lesevorgänge innerhalb einer optimistischen Transaktion auf demselben Snapshot-Zeitstempel basieren, muss die Konsistenz während der Ausführung oder des Commits für diese Lesevorgänge nicht überprüft werden. Dadurch wird die Latenz erheblich reduziert.

Risiken der optimistischen Gleichzeitigkeitserkennung

Die optimistische Parallelität birgt Risiken, insbesondere bei hoher Lese-/Schreibkonkurrenz in Verbindung mit serialisierbarer Isolation. Machen Sie sich mit diesen Risiken vertraut, bevor Sie die optimistische Parallelitätssteuerung mit serialisierbarer Isolation für Ihre Arbeitslast verwenden.

  • Bei hoher Lese-/Schreibkonkurrenz kann es bei optimistischen Transaktionen zu einer hohen Anzahl von Abbrüchen kommen, da gleichzeitige Schreibvorgänge die Lesevorgänge einer optimistischen Transaktion ungültig machen können.
  • Bei anhaltend hoher Konfliktwahrscheinlichkeit kann es sein, dass eine Transaktion wiederholt abgebrochen wird und aufgrund von Transaktions-Starvation nie committet wird.

Anwendungsfälle für optimistische Gleichzeitigkeitserkennung

Optimistische Parallelität eignet sich für transaktionale Arbeitslasten mit geringen Lese-/Schreibkonflikten. Bei serialisierbaren Transaktionen profitieren auch Arbeitslasten, die Transaktionsabbrüche tolerieren können.

Erwägen Sie die optimistische Parallelität für die folgenden Arbeitslasten:

  • Arbeitslasten mit niedriger Priorität und latenzunempfindlichen, lang andauernden Transaktionen:Verwenden Sie optimistische Parallelität, wenn lang andauernde Lese- oder Abfragevorgänge latenzempfindliche Schreibvorgänge verzögern könnten. So werden Verzögerungen durch Lesesperren vermieden. Beispiele: Transaktionen in mobilen Clients mit langsamen Verbindungen oder Transaktionen mit niedrigem SLA, die Lesesperren für viele Zeilen oder große Bereiche enthalten.
  • Transaktionale Arbeitslasten mit geringer Lese-/Schreibkonkurrenz, bei denen die Leselatenz wichtig ist: Verwenden Sie in einer multiregionalen Konfiguration optimistische Parallelität, um Lesevorgänge regional auszuführen, Leselatenzen zu reduzieren und Produktionsprobleme durch Spitzen beim Lesetraffic für einen Hot Split zu vermeiden. Außerdem wird die Leseverfügbarkeit bei Überlastung oder Nichtverfügbarkeit des Leaders verbessert.
  • Transaktionale Arbeitslasten, bei denen die meisten Transaktionen schreibgeschützt sind:Durch die Umstellung auf optimistische Parallelität wird die Commit-Latenz für häufige schreibgeschützte Transaktionen in diesen Arbeitslasten reduziert. Achten Sie auf geringe Konflikte bei Lese-/Schreibvorgängen, um hohe Abbruchraten für Lese-/Schreibtransaktionen zu vermeiden.

Vermeiden Sie die Verwendung der optimistischen Gleichzeitigkeitserkennung für latenzempfindliche transaktionale Arbeitslasten, bei denen Lese-/Schreibkonflikte häufig auftreten.

Nebenläufigkeitserkennung konfigurieren

Sie können die Spanner-Clientbibliotheken, die REST API und die RPC API verwenden, um den Parallelitätsmodus für Lese-/Schreibtransaktionen anzugeben.

Clientbibliotheken

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

Die Spanner TransactionOptions REST API bietet ein ReadLockMode-Enum in der ReadWrite-Nachricht, mit dem Sie entweder den PESSIMISTIC- oder den OPTIMISTIC-Sperrmodus auswählen können.

RPC

Die Spanner-RPC-API Transactionoptions bietet ein ReadLockMode-Enum innerhalb der ReadWrite-Nachricht, mit dem Sie entweder den PESSIMISTIC- oder den OPTIMISTIC-Sperrmodus auswählen können.

Erfolgsfaktoren

Sie können die Spanner-Treiber verwenden, um read_lock_mode als Verbindungsparameter auf Verbindungsebene oder als SET-Anweisungsoption auf Transaktionsebene festzulegen. Weitere Informationen zu den einzelnen Treibern finden Sie unter Übersicht über Treiber.

Nächste Schritte