Control de simultaneidad

Las transacciones de Spanner ofrecen dos modos de control de simultaneidad: pesimista y optimista. La elección del modo de control de simultaneidad afecta la forma en que las transacciones controlan las lecturas y escrituras simultáneas, lo que influye en el rendimiento, la latencia y las tasas de anulación de transacciones. Elige el modo que mejor se adapte a los requisitos de rendimiento y coherencia de tu aplicación.

El comportamiento predeterminado depende del nivel de aislamiento que use tu transacción:

Control de simultaneidad pesimista

De forma predeterminada, Spanner usa la simultaneidad pesimista con aislamiento serializable. También puedes usar la simultaneidad pesimista con aislamiento de lectura repetible.

Simultaneidad pesimista en aislamiento serializable

Este modo supone que las transacciones simultáneas podrían competir por los mismos datos. Adquiere bloqueos de forma proactiva en los datos a medida que se leen o escriben dentro de una transacción. También verifica que los bloqueos adquiridos anteriormente en la transacción permanezcan en las instrucciones posteriores. Cuando Spanner detecta un conflicto de bloqueo, usa el algoritmo de prevención para resolverlo.

En la simultaneidad pesimista, las transacciones adquieren bloqueos en los datos durante las fases de ejecución y confirmación de la transacción.

  • Para las lecturas: Cuando una transacción lee datos, adquiere un bloqueo de lectura compartida (ReaderShared) durante la fase de ejecución. Estos bloqueos se mantienen hasta que se confirma la transacción.
  • Para DML y escrituras:
    • Durante la ejecución, para los datos modificados por DML o escrituras, la transacción podría adquirir bloqueos de lectura en la existencia de filas.
    • En el momento de la confirmación, la transacción intenta adquirir bloqueos de escritura o exclusivos para los datos escritos. Los bloqueos de escritura bloquean las lecturas simultáneas, pero es posible que no bloqueen las escrituras simultáneas, en especial cuando ambas usan bloqueos de escritura. Esto significa que varias transacciones pueden continuar con la confirmación, y los conflictos de escritura y escritura se resuelven en el momento de la confirmación con el algoritmo de prevención. Todos los bloqueos se mantienen hasta que se confirma la transacción.

Simultaneidad pesimista en aislamiento de lectura repetible

Usa la simultaneidad pesimista en el aislamiento de lectura repetible para serializar las escrituras. En este modo, las operaciones de lectura usan instantáneas, pero los bloqueos exclusivos se aplican a los datos leídos de las consultas FOR UPDATE o las lock_scanned_ranges=exclusive sugerencias, y a los datos escritos con consultas DML.

Beneficios de la simultaneidad pesimista con aislamiento serializable

El principal beneficio de usar la simultaneidad pesimista con aislamiento serializable es que, en cargas de trabajo muy contenciosas, ayuda a que las transacciones progresen. Spanner prioriza las transacciones más antiguas sobre las más recientes durante los conflictos, lo que garantiza que las transacciones se completen y, al mismo tiempo, reduce la cantidad de transacciones que se anulan de forma repetida.

Beneficios de la simultaneidad pesimista con aislamiento de lectura repetible

Con el aislamiento de lectura repetible, las transacciones que adquieren bloqueos aún pueden anularse en el momento de la confirmación si los datos leídos como parte de una consulta con FOR UPDATE o como parte de una consulta DML fueron modificados por una transacción simultánea antes de que se confirme la transacción. Sin embargo, después de adquirir los bloqueos, evita más actualizaciones simultáneas hasta que se confirme la transacción, lo que serializa las escrituras.

Riesgos de la simultaneidad pesimista

La simultaneidad pesimista con aislamiento serializable presenta los siguientes riesgos:

  • Las lecturas de larga duración pueden bloquear las escrituras sensibles a la latencia.
  • Las transacciones que implican la interacción del usuario antes de la finalización pueden hacer que los bloqueos se mantengan durante un período prolongado, lo que podría bloquear otras operaciones.

Casos de uso de la simultaneidad pesimista con aislamiento serializable

La simultaneidad pesimista es adecuada para cargas de trabajo con alta contención de lectura y escritura, y de escritura y escritura. También es apropiada cuando las anulaciones y los reintentos de transacciones son costosos. Usa este modo predeterminado, a menos que tu carga de trabajo tenga retrasos excesivos de bloqueo prolongado o se vea afectada de manera significativa por los conflictos de bloqueo.

Casos de uso de la simultaneidad pesimista con aislamiento de lectura repetible

Usa la simultaneidad pesimista con lectura repetible para cargas de trabajo que requieran una cláusula FOR UPDATE o consultas DML para adquirir bloqueos. Este enfoque es especialmente útil para las cargas de trabajo migradas a Spanner desde otras bases de datos que adquieren bloqueos para estas instrucciones.

Control de simultaneidad optimista

Spanner también proporciona control de simultaneidad optimista. Cuando usas el aislamiento de lectura repetible, el modo predeterminado es el control de simultaneidad optimista. También puedes configurar el aislamiento serializable para usar el control de simultaneidad optimista.

El control de simultaneidad optimista supone que los conflictos son poco frecuentes. Las lecturas y las consultas, incluso dentro de una transacción de lectura y escritura, continúan sin adquirir bloqueos. Con el aislamiento serializable predeterminado de Spanner, las lecturas se validan en el momento de la confirmación. Esto garantiza que ninguna otra transacción confirmada de forma simultánea haya modificado los datos leídos anteriormente por la transacción. Si usas el aislamiento de lectura repetible, las lecturas con una sugerencia FOR UPDATE o lock_scanned_ranges=exclusive se validan en el momento de la confirmación. Si Spanner detecta un conflicto, anula la transacción.

Cómo funciona la simultaneidad optimista

La simultaneidad optimista cambia la forma en que Spanner ejecuta lecturas, consultas y confirma transacciones. Realiza una ejecución sin bloqueo durante la fase de lectura y valida la coherencia en la confirmación.

Para lecturas y consultas

Las lecturas y las consultas no tienen bloqueo. Todas las lecturas y consultas dentro de una transacción optimista se ejecutan en una sola marca de tiempo de instantánea. Spanner elige esta marca de tiempo cuando se ejecuta la primera lectura o consulta. Esto garantiza que todas las lecturas y consultas posteriores dentro de la transacción vean las escrituras confirmadas antes de la primera lectura o consulta.

Para lecturas y escrituras

Para una transacción optimista con lecturas y escrituras, Spanner realiza un paso de validación en el momento de la confirmación. La transacción se confirma correctamente solo si no se detectan conflictos y se cumplen las siguientes condiciones:

  • Ninguna escritura confirmada de forma simultánea entra en conflicto con los datos leídos por esta transacción; es decir, no se confirmó ninguna escritura después de la marca de tiempo de lectura, pero antes de que esta transacción confirme sus propias escrituras.
  • El esquema no se modificó desde la marca de tiempo de lectura.

El nivel de aislamiento determina el conjunto de lecturas que se validan. Con el aislamiento serializable, se validan todas las lecturas. Con el aislamiento de lectura repetible, las lecturas con una sugerencia FOR UPDATE o lock_scanned_ranges=exclusive se validan en el momento de la confirmación.

En condiciones de alta contención, las transacciones optimistas pueden anularse de forma repetida. Por el contrario, las transacciones pesimistas resuelven los conflictos de lectura y escritura permitiendo que se confirme la transacción más antigua y reintentando la transacción más reciente.

Beneficios de la simultaneidad optimista

La simultaneidad optimista ofrece los siguientes beneficios:

  • Las lecturas no adquieren bloqueos: Las transacciones optimistas no adquieren bloqueos para las lecturas, por lo que las lecturas de larga duración no bloquean las escrituras sensibles a la latencia.
  • Latencia de confirmación reducida para transacciones de solo lectura: Debido a que todas las lecturas dentro de una transacción optimista se basan en la misma marca de tiempo de instantánea, no es necesario verificar la coherencia durante la ejecución o la confirmación de estas lecturas, lo que reduce significativamente la latencia.

Riesgos de la simultaneidad optimista

La simultaneidad optimista introduce riesgos, en especial en condiciones de alta contención de lectura y escritura cuando se usa con aislamiento serializable. Comprende estos riesgos antes de usar el control de simultaneidad optimista con aislamiento serializable para tu carga de trabajo.

  • En condiciones de alta contención de lectura y escritura, las transacciones optimistas pueden experimentar una alta tasa de anulaciones, ya que las escrituras simultáneas pueden invalidar las lecturas de una transacción optimista.
  • Con una contención alta persistente, una transacción puede anularse de forma repetida y nunca confirmarse por falta de transacción.

Casos de uso de la simultaneidad optimista

La simultaneidad optimista es adecuada para cargas de trabajo transaccionales con baja contención de lectura y escritura. Para las transacciones serializables, también beneficia a las cargas de trabajo que pueden tolerar las anulaciones de transacciones.

Considera la simultaneidad optimista para las siguientes cargas de trabajo:

  • Cargas de trabajo de baja prioridad y tolerantes a la latencia con transacciones de larga duración: Usa la simultaneidad optimista si las lecturas o consultas de larga duración pueden retrasar las escrituras sensibles a la latencia. Esto evita los retrasos causados por los bloqueos de lectura. Por ejemplo, las transacciones en clientes móviles con conexiones lentas o las transacciones de bajo ANS que mantienen bloqueos de lectura para muchas filas o rangos grandes.
  • Cargas de trabajo transaccionales sensibles a la latencia de lectura con baja contención de lectura y escritura: En una configuración multirregional, usa la simultaneidad optimista para entregar lecturas de forma regional, reducir las latencias de lectura y evitar problemas de producción debido al tráfico de lectura con picos a una división activa. También mejora la disponibilidad de lectura durante la sobrecarga o la falta de disponibilidad del líder.
  • Cargas de trabajo transaccionales en las que la mayoría de las transacciones son de solo lectura: Cambiar a la simultaneidad optimista reduce la latencia de confirmación para las transacciones comunes de solo lectura en estas cargas de trabajo. Asegúrate de que haya una baja contención de lectura y escritura para evitar tasas de anulación altas para las transacciones de lectura y escritura.

Evita usar la simultaneidad optimista para cargas de trabajo transaccionales sensibles a la latencia en las que los conflictos de lectura y escritura son frecuentes.

Configura el control de simultaneidad

Puedes usar las bibliotecas cliente, la API de REST y la API de RPC de Spanner para especificar el modo de simultaneidad para las transacciones de lectura y escritura.

Bibliotecas cliente

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

La API de REST de TransactionOptions de Spanner proporciona una enumeración ReadLockMode dentro del mensaje ReadWrite que te permite seleccionar el modo de bloqueo PESSIMISTIC o OPTIMISTIC.

RPC

La API de RPC de Transactionoptions de Spanner proporciona una enumeración ReadLockMode dentro del mensaje ReadWrite que te permite seleccionar el modo de bloqueo PESSIMISTIC o OPTIMISTIC.

Controladores

Puedes usar los controladores de Spanner para establecer read_lock_mode como un parámetro de conexión en el nivel de conexión o como una opción de instrucción SET en el nivel de transacción. Para obtener más información sobre cada controlador, consulta Descripción general de los controladores.

¿Qué sigue?