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 le letture e le scritture simultanee, influenzando le prestazioni, la latenza e le percentuali 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. Puoi anche utilizzare la concorrenza pessimistica con l'isolamento di lettura ripetibile.

Concorrenza pessimistica nell'isolamento serializzabile

Questa modalità presuppone che le transazioni simultanee possano competere per gli stessi dati. Acquisisce i 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.

Nella concorrenza pessimistica, 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 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 entrambe 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.

Concorrenza pessimistica nell'isolamento di lettura ripetibile

Utilizza la concorrenza pessimistica nell'isolamento di lettura ripetibile per serializzare le scritture. In questa modalità, le operazioni di lettura utilizzano gli snapshot, ma i blocchi esclusivi si applicano ai dati letti dalle FOR UPDATE query o dai lock_scanned_ranges=exclusive suggerimenti e ai dati scritti con le query DML.

Vantaggi della concorrenza pessimistica con l'isolamento serializzabile

Il vantaggio principale dell'utilizzo della concorrenza pessimistica con l'isolamento serializzabile è che, nei carichi di lavoro altamente contenziosi, aiuta le transazioni a progredire. Spanner assegna la priorità alle transazioni precedenti rispetto a quelle più recenti durante i conflitti, assicurandosi che le transazioni vengano completate e riducendo al contempo il numero di transazioni interrotte ripetutamente.

Vantaggi della concorrenza pessimistica con l'isolamento di lettura ripetibile

Con l'isolamento di lettura ripetibile, le transazioni che acquisiscono blocchi potrebbero comunque essere interrotte al momento del commit se i dati letti come parte di una query con FOR UPDATE o come parte di una query DML sono stati modificati da una transazione simultanea prima del commit della transazione. Tuttavia, dopo l'acquisizione dei blocchi, impedisce ulteriori aggiornamenti simultanei fino al commit della transazione, serializzando le scritture.

Rischi della concorrenza pessimistica

La concorrenza pessimistica con l'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 il mantenimento dei blocchi per un lungo periodo di tempo, bloccando potenzialmente altre operazioni.

Casi d'uso della concorrenza pessimistica con l'isolamento serializzabile

La concorrenza pessimistica è adatta ai carichi di lavoro con elevata contesa di lettura-scrittura e scrittura-scrittura. È appropriata anche quando le interruzioni e i nuovi tentativi delle transazioni sono costosi. Utilizza questa modalità predefinita a meno che il tuo carico di lavoro non presenti ritardi eccessivi nei blocchi a lunga esecuzione o non sia influenzato in modo significativo dai conflitti di blocco.

Casi d'uso della concorrenza pessimistica con l'isolamento di lettura ripetibile

Utilizza la concorrenza pessimistica con la lettura ripetibile per i carichi di lavoro che richiedono una clausola FOR UPDATE o query DML per acquisire i blocchi. Questo approccio è particolarmente utile per i carichi di lavoro di cui è stata eseguita la migrazione a Spanner da altri database che acquisiscono blocchi per queste istruzioni.

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 in modo che utilizzi controllo della contemporaneità ottimistico.

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. In questo modo, nessuna altra transazione di cui è stato eseguito il commit simultaneamente ha 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 momento del commit.

Per letture e query

Le letture e le query non sono bloccate. Tutte le letture e le query all'interno di una transazione ottimistica vengono eseguite in un singolo timestamp di 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 di cui è stato eseguito il commit prima della prima lettura o query.

Per letture e scritture

Per una transazione ottimistica con letture e scritture, Spanner esegue un passaggio di convalida al momento del commit. Il commit della transazione viene eseguito correttamente solo se non vengono rilevati conflitti e se vengono soddisfatte le seguenti condizioni:

  • Nessuna scrittura di cui è stato eseguito il commit simultaneamente è in conflitto con i dati letti da questa transazione, ovvero non sono stati eseguiti commit di scritture dopo il timestamp di lettura, ma prima del commit delle scritture di questa transazione.
  • Lo schema non è stato modificato dal 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 elevata contesa, le transazioni ottimistiche potrebbero essere interrotte ripetutamente. Al contrario, le transazioni pessimistiche risolvono i conflitti di lettura-scrittura consentendo il 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 di 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 elevata contesa di lettura-scrittura quando viene utilizzata con l'isolamento serializzabile. Comprendi questi rischi prima di utilizzare controllo della contemporaneità ottimistico con l'isolamento serializzabile per il tuo carico di lavoro.

  • In caso di elevata contesa di lettura-scrittura, le transazioni ottimistiche potrebbero riscontrare un'elevata percentuale di interruzioni, 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 della mancanza di transazioni.

Casi d'uso della concorrenza ottimistica

La concorrenza ottimistica è adatta ai carichi di lavoro transazionali con bassa contesa di lettura-scrittura. Per le transazioni serializzabili, è utile anche per i carichi di lavoro che possono tollerare le interruzioni delle transazioni.

Valuta la concorrenza ottimistica per i seguenti carichi di lavoro:

  • 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 i ritardi causati dai blocchi di lettura. Ad esempio, le transazioni nei client mobile con connessioni lente o le transazioni con SLA basso che mantengono i 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 pubblicare le letture a livello regionale, ridurre le latenze di lettura ed evitare problemi di produzione dovuti al traffico di lettura con picchi in una suddivisione attiva. Migliora anche la disponibilità di lettura durante il sovraccarico o l'indisponibilità 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 percentuali di interruzione elevate 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.

Configurare 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();
    }
}

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

L'API REST TransactionOptions di Spanner fornisce un'enumerazione ReadLockMode all'interno del messaggio ReadWrite che ti 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 ti 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 ogni driver, consulta la panoramica dei driver.

Passaggi successivi