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 simultaneidad pesimista con aislamiento serializable. En este modo, se 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 se mantengan 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 la fila.
    • 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 ambos usan bloqueos de escritura. Esto significa que varias transacciones pueden proceder a la confirmación, y los conflictos de escritura-escritura se resuelven en el momento de la confirmación con el algoritmo de herida y espera. Todos los bloqueos se mantienen hasta que se confirma la transacción.

Beneficios de la simultaneidad pesimista con aislamiento serializable

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

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 completarse pueden hacer que los bloqueos se mantengan durante mucho tiempo, lo que podría bloquear otras operaciones.

Casos de uso de la simultaneidad pesimista

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

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, se realizan 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 que leyó la transacción. Si usas el aislamiento de lectura repetible, las lecturas con una sugerencia de 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 la 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 bloqueos. 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

En el caso de 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:

  • No hay escrituras confirmadas de forma simultánea que entren en conflicto con los datos leídos por esta transacción, es decir, no se confirmaron escrituras 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 de FOR UPDATE o lock_scanned_ranges=exclusive se validan en el momento de la confirmación.

En situaciones de alta contención, es posible que las transacciones optimistas se anulen de forma reiterada. En cambio, 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 las transacciones de solo lectura: Dado 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 particular, 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, es posible que una transacción se anule repetidamente y nunca se confirme debido a la inanición de la 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. En el caso de las transacciones serializables, también beneficia a las cargas de trabajo que pueden tolerar la anulación 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 las demoras causadas por los bloqueos de lectura. Por ejemplo, transacciones en clientes móviles con conexiones lentas o transacciones con un ANS bajo 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 en 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 poca contención de lectura y escritura para evitar tasas altas de cancelación de transacciones de lectura y escritura.

Evita usar la simultaneidad optimista para las 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 de 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();
    }
}

REST

La API de REST de TransactionOptions de Spanner proporciona un enum 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 un enum 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 a nivel de la conexión o como una opción de la instrucción read_lock_mode a nivel de la transacción.SET Para obtener más información sobre cada controlador, consulta la Descripción general de los controladores.

¿Qué sigue?