Controllo della concorrenza

Le transazioni Spanner offrono due modalità di controllo della contemporaneità: pessimistica e ottimistica. La scelta della modalità di controllo della contemporaneità influisce sul modo in cui le transazioni gestiscono letture e scritture simultanee, influenzando prestazioni, latenza e tassi di interruzione delle transazioni. Scegli la modalità più adatta ai requisiti di prestazioni e coerenza della tua applicazione.

Il comportamento predefinito dipende dal livello di isolamento utilizzato dalla transazione:

Controllo della contemporaneità pessimistico

Per impostazione predefinita, Spanner utilizza la concorrenza pessimistica con l'isolamento serializzabile. Questa modalità presuppone che le transazioni simultanee possano competere per gli stessi dati. Acquisisce blocchi in modo proattivo sui dati durante la lettura o la scrittura all'interno di una transazione. Verifica inoltre che i blocchi acquisiti in precedenza nella transazione rimangano attivi nelle istruzioni successive. Quando Spanner rileva un conflitto di blocco, utilizza l'algoritmo wound-wait per risolverlo.

Nel controllo della concorrenza pessimistico, le transazioni acquisiscono blocchi sui dati durante le fasi di esecuzione e commit della transazione.

  • Per le letture:quando una transazione legge i dati, acquisisce un blocco di lettura condiviso (ReaderShared) durante la fase di esecuzione. Questi blocchi vengono mantenuti fino al commit della transazione.
  • Per DML e scritture:
    • Durante l'esecuzione, per i dati modificati da DML o dalle scritture, la transazione potrebbe acquisire blocchi di lettura sull'esistenza delle righe.
    • Al momento del commit, la transazione tenta di acquisire blocchi di scrittura o esclusivi per i dati scritti. I blocchi di scrittura bloccano le letture simultanee, ma potrebbero non bloccare le scritture simultanee, soprattutto quando entrambi utilizzano blocchi di scrittura. Ciò significa che più transazioni possono procedere al commit e i conflitti di scrittura-scrittura vengono risolti al momento del commit utilizzando l'algoritmo wound-wait. Tutti i blocchi vengono mantenuti fino al commit della transazione.

Vantaggi della concorrenza pessimistica con isolamento serializzabile

Il vantaggio principale dell'utilizzo della concorrenza pessimistica con l'isolamento serializzabile è che, nei carichi di lavoro altamente controversi, aiuta le transazioni a progredire. Spanner dà la priorità alle transazioni meno recenti rispetto a quelle più recenti durante i conflitti, garantendo che le transazioni vengano completate alla fine riducendo la quantità di transazioni interrotte ripetutamente.

Rischi della concorrenza pessimistica

La concorrenza pessimistica con isolamento serializzabile presenta i seguenti rischi:

  • Le letture a lunga esecuzione potrebbero bloccare le scritture sensibili alla latenza.
  • Le transazioni che prevedono l'interazione dell'utente prima del completamento potrebbero causare blocchi per un lungo periodo di tempo, bloccando potenzialmente altre operazioni.

Casi d'uso per la concorrenza pessimistica

La concorrenza pessimistica è adatta per i workload con elevata contesa di lettura/scrittura e scrittura/scrittura. È appropriato anche quando gli annullamenti e i nuovi tentativi di transazione sono costosi. Utilizza questa modalità predefinita, a meno che il tuo workload non presenti ritardi eccessivi di blocco prolungato o non sia interessato in modo significativo da conflitti di blocco.

Controllo della contemporaneità ottimistico

Spanner fornisce anche controllo della contemporaneità ottimistico. Quando utilizzi l'isolamento di lettura ripetibile, la modalità predefinita è il controllo della concorrenza ottimistico. Puoi anche configurare l'isolamento serializzabile per utilizzare controllo della contemporaneità ottimistico.

Il controllo della contemporaneità ottimistico presuppone che i conflitti siano rari. Le letture e le query anche all'interno di una transazione di lettura/scrittura procedono senza acquisire blocchi. Con l'isolamento serializzabile predefinito di Spanner, le letture vengono convalidate al momento del commit. Ciò garantisce che nessun'altra transazione di cui è stato eseguito il commit contemporaneamente abbia modificato i dati letti in precedenza dalla transazione. Se utilizzi l'isolamento di lettura ripetibile, le letture con un suggerimento FOR UPDATE o lock_scanned_ranges=exclusive vengono convalidate al momento del commit. Se Spanner rileva un conflitto, interrompe la transazione.

Come funziona la concorrenza ottimistica

La concorrenza ottimistica modifica il modo in cui Spanner esegue le letture, le query e i commit delle transazioni. Esegue l'esecuzione senza blocchi durante la fase di lettura e convalida la coerenza al commit.

Per letture e query

Le letture e le query sono senza blocchi. Tutte le letture e le query all'interno di una transazione ottimistica vengono eseguite in un unico timestamp dello snapshot. Spanner sceglie questo timestamp quando viene eseguita la prima lettura o query. In questo modo, tutte le letture e le query successive all'interno della transazione vedono le scritture eseguite prima della prima lettura o query.

Per letture e scritture

Per una transazione ottimistica con operazioni di lettura e scrittura, Spanner esegue un passaggio di convalida al momento del commit. Il commit della transazione va a buon fine solo se non vengono rilevati conflitti e sono soddisfatte le seguenti condizioni:

  • Nessuna scrittura di cui è stato eseguito il commit contemporaneamente è in conflitto con i dati letti da questa transazione, ovvero non sono state eseguite scritture dopo il timestamp di lettura ma prima che questa transazione esegua il commit delle proprie scritture.
  • Lo schema non è stato modificato dall'ultimo timestamp di lettura.

Il livello di isolamento determina l'insieme di letture convalidate. Con l'isolamento serializzabile, tutte le letture vengono convalidate. Con l'isolamento di lettura ripetibile, le letture con un suggerimento FOR UPDATE o lock_scanned_ranges=exclusive vengono convalidate al momento del commit.

In caso di contesa elevata, le transazioni ottimistiche potrebbero essere interrotte ripetutamente. Al contrario, le transazioni pessimistiche risolvono i conflitti di lettura/scrittura consentendo l'esecuzione del commit della transazione precedente e riprovando la transazione più recente.

Vantaggi della concorrenza ottimistica

La concorrenza ottimistica offre i seguenti vantaggi:

  • Le letture non acquisiscono blocchi: le transazioni ottimistiche non acquisiscono blocchi per le letture, quindi le letture a lunga esecuzione non bloccano le scritture sensibili alla latenza.
  • Latenza di commit ridotta per le transazioni di sola lettura: poiché tutte le letture all'interno di una transazione ottimistica si basano sullo stesso timestamp dello snapshot, non è necessario verificare la coerenza durante l'esecuzione o il commit per queste letture, il che riduce significativamente la latenza.

Rischi della concorrenza ottimistica

La concorrenza ottimistica introduce rischi, in particolare in caso di contesa di lettura/scrittura elevata se utilizzata con l'isolamento serializzabile. Comprendi questi rischi prima di utilizzare controllo della contemporaneità ottimistico con l'isolamento serializzabile per il tuo workload.

  • In caso di contesa di lettura/scrittura elevata, le transazioni ottimistiche potrebbero subire un tasso di interruzioni elevato, perché le scritture simultanee potrebbero invalidare le letture di una transazione ottimistica.
  • In caso di contesa elevata persistente, una transazione potrebbe essere interrotta ripetutamente e non essere mai eseguita a causa di starvation della transazione.

Casi d'uso per la concorrenza ottimistica

La concorrenza ottimistica è adatta per i workload transazionali con bassa contesa di lettura/scrittura. Per le transazioni serializzabili, avvantaggia anche i carichi di lavoro che possono tollerare gli annullamenti delle transazioni.

Prendi in considerazione la concorrenza ottimistica per i seguenti workload:

  • Carichi di lavoro a bassa priorità e tolleranti alla latenza con transazioni a lunga esecuzione:utilizza la concorrenza ottimistica se le letture o le query a lunga esecuzione potrebbero ritardare le scritture sensibili alla latenza. In questo modo si evitano ritardi causati dai blocchi di lettura. Ad esempio, transazioni in client mobile con connessioni lente o transazioni con SLA basso che mantengono blocchi di lettura per molte righe o intervalli di grandi dimensioni.
  • Carichi di lavoro transazionali sensibili alla latenza di lettura con bassa contesa di lettura/scrittura: In una configurazione multiregionale, utilizza la concorrenza ottimistica per gestire le letture a livello regionale, ridurre le latenze di lettura ed evitare problemi di produzione dovuti a picchi di traffico di lettura in una suddivisione calda. Migliora inoltre la disponibilità di lettura durante il sovraccarico o la mancata disponibilità del leader.
  • Carichi di lavoro transazionali in cui la maggior parte delle transazioni è di sola lettura: il passaggio alla concorrenza ottimistica riduce la latenza di commit per le transazioni di sola lettura comuni in questi carichi di lavoro. Assicurati che la contesa di lettura/scrittura sia bassa per evitare tassi di interruzione elevati per le transazioni di lettura/scrittura.

Evita di utilizzare la concorrenza ottimistica per i carichi di lavoro transazionali sensibili alla latenza in cui i conflitti di lettura/scrittura sono frequenti.

Configura controllo della contemporaneità

Puoi utilizzare le librerie client, l'API REST e l'API RPC di Spanner per specificare la modalità di concorrenza per le transazioni di lettura/scrittura.

Librerie client

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

L'API REST TransactionOptions di Spanner fornisce un'enumerazione ReadLockMode all'interno del messaggio ReadWrite che consente di selezionare la modalità di blocco PESSIMISTIC o OPTIMISTIC.

RPC

L'API RPC Transactionoptions di Spanner fornisce un'enumerazione ReadLockMode all'interno del messaggio ReadWrite che consente di selezionare la modalità di blocco PESSIMISTIC o OPTIMISTIC.

Driver

Puoi utilizzare i driver di Spanner per impostare read_lock_mode come parametro di connessione a livello di connessione o come opzione dell'istruzione SET a livello di transazione. Per saperne di più su ciascun driver, consulta la panoramica dei driver.

Passaggi successivi