Controle de simultaneidade

As transações do Spanner oferecem dois modos de controle de simultaneidade: pessimista e otimista. A escolha do modo de controle de simultaneidade afeta a forma como as transações lidam com leituras e gravações simultâneas, influenciando o desempenho, a latência e as taxas de interrupção de transações. Escolha o modo que melhor atende aos requisitos de desempenho e consistência do seu aplicativo.

O comportamento padrão depende do nível de isolamento usado pela transação:

Controle de simultaneidade pessimista

Por padrão, o Spanner usa simultaneidade pessimista com isolamento serializável. Esse modo pressupõe que transações simultâneas podem disputar os mesmos dados. Ele adquire bloqueios de maneira proativa nos dados à medida que são lidos ou gravados em uma transação. Ele também verifica se os bloqueios adquiridos anteriormente na transação permanecem em instruções posteriores. Quando o Spanner detecta um conflito de bloqueio, ele usa o algoritmo wound-wait para resolver o problema.

Na simultaneidade pessimista, as transações adquirem bloqueios nos dados durante as fases de execução e confirmação da transação.

  • Para leituras:quando uma transação lê dados, ela adquire um bloqueio de leitura compartilhada (ReaderShared) durante a fase de execução. Esses bloqueios são mantidos até que a transação seja confirmada.
  • Para DML e gravações:
    • Durante a execução, para dados modificados por DML ou gravações, a transação pode adquirir bloqueios de leitura na existência de linhas.
    • No momento da confirmação, a transação tenta adquirir bloqueios de gravação ou exclusivos para os dados gravados. Os bloqueios de gravação bloqueiam leituras simultâneas, mas podem não bloquear gravações simultâneas, especialmente quando ambos usam bloqueios de gravação. Isso significa que várias transações podem prosseguir para o commit, e os conflitos de gravação-gravação são resolvidos no momento do commit usando o algoritmo wound-wait. Todos os bloqueios são mantidos até que a transação seja confirmada.

Benefícios da simultaneidade pessimista com isolamento serializável

O principal benefício de usar simultaneidade pessimista com isolamento serializável é que, em cargas de trabalho altamente disputadas, ela ajuda as transações a progredir. O Spanner prioriza transações mais antigas em vez de mais recentes durante conflitos, garantindo que as transações sejam concluídas e reduzindo a quantidade de transações canceladas repetidamente.

Riscos da simultaneidade pessimista

A simultaneidade pessimista com isolamento serializável apresenta os seguintes riscos:

  • Leituras de longa duração podem bloquear gravações sensíveis à latência.
  • Transações que envolvem a interação do usuário antes da conclusão podem fazer com que os bloqueios sejam mantidos por muito tempo, o que pode impedir outras operações.

Casos de uso de simultaneidade pessimista

A simultaneidade pessimista é adequada para cargas de trabalho com alta contenção de leitura-gravação e gravação-gravação. Também é adequado quando os abortos e as novas tentativas de transação são caros. Use esse modo padrão, a menos que sua carga de trabalho tenha atrasos excessivos de bloqueio longo ou seja significativamente afetada por conflitos de bloqueio.

Controle de simultaneidade otimista

O Spanner também oferece controle de simultaneidade otimista. Quando você usa o isolamento de leitura repetível, o modo padrão é o controle de simultaneidade otimista. Também é possível configurar o isolamento serializável para usar o controle de simultaneidade otimista.

O controle de simultaneidade otimista pressupõe que os conflitos são raros. Leituras e consultas, mesmo em uma transação de leitura e gravação, são feitas sem adquirir bloqueios. Com o isolamento serializável padrão do Spanner, as leituras são validadas no momento da confirmação. Isso garante que nenhuma outra transação confirmada simultaneamente tenha modificado os dados lidos anteriormente pela transação. Se você usar o isolamento de leitura repetível, as leituras com uma dica FOR UPDATE ou lock_scanned_ranges=exclusive serão validadas no momento do commit. Se o Spanner detectar um conflito, ele vai cancelar a transação.

Como funciona a simultaneidade otimista

A simultaneidade otimista muda a forma como o Spanner executa leituras, consultas e confirmações de transações. Ela realiza a execução sem bloqueio durante a fase de leitura e valida a consistência no commit.

Para leituras e consultas

Leituras e consultas são sem bloqueio. Todas as leituras e consultas em uma transação optimista são executadas em um único carimbo de data/hora de snapshot. O Spanner escolhe esse carimbo de data/hora quando a primeira leitura ou consulta é executada. Isso garante que todas as leituras e consultas subsequentes na transação vejam as gravações confirmadas antes da primeira leitura ou consulta.

Para leituras e gravações

Para uma transação otimista com leituras e gravações, o Spanner executa uma etapa de validação no momento da confirmação. A transação será confirmada com êxito somente se nenhum conflito for detectado e as seguintes condições forem atendidas:

  • Nenhuma gravação confirmada simultaneamente entra em conflito com os dados lidos por esta transação. Ou seja, nenhuma gravação foi confirmada após o carimbo de data/hora da leitura, mas antes que essa transação confirme as próprias gravações.
  • O esquema não foi modificado desde o carimbo de data/hora da leitura.

O nível de isolamento determina o conjunto de leituras que são validadas. Com o isolamento serializável, todas as leituras são validadas. Com o isolamento de leitura repetível, as leituras com uma dica FOR UPDATE ou lock_scanned_ranges=exclusive são validadas no momento da confirmação.

Em alta disputa, as transações otimistas podem ser canceladas repetidamente. Em contraste, as transações pessimistas resolvem conflitos de leitura/gravação permitindo que a transação mais antiga seja confirmada e repetindo a transação mais recente.

Benefícios da simultaneidade otimista

A simultaneidade otimista oferece os seguintes benefícios:

  • As leituras não adquirem bloqueios: as transações otimistas não adquirem bloqueios para leituras. Portanto, leituras de longa duração não bloqueiam gravações sensíveis à latência.
  • Latência de confirmação reduzida para transações somente leitura: como todas as leituras em uma transação otimista são baseadas no mesmo carimbo de data/hora de snapshot, não é necessário verificar a consistência durante a execução ou confirmação dessas leituras, o que reduz significativamente a latência.

Riscos da simultaneidade otimista

A simultaneidade otimista apresenta riscos, principalmente em alta disputa de leitura/gravação quando usada com isolamento serializável. Entenda esses riscos antes de usar o controle de simultaneidade otimista com isolamento serializável para sua carga de trabalho.

  • Em alta disputa de leitura e gravação, as transações otimistas podem ter uma alta taxa de interrupções, porque gravações simultâneas podem invalidar as leituras de uma transação otimista.
  • Com alta contenção persistente, uma transação pode ser cancelada repetidamente e nunca ser confirmada devido à falta de transações.

Casos de uso para simultaneidade otimista

A simultaneidade otimista é adequada para cargas de trabalho transacionais com baixa disputa de leitura/gravação. Para transações serializáveis, ele também beneficia cargas de trabalho que podem tolerar a interrupção de transações.

Considere a simultaneidade otimista para as seguintes cargas de trabalho:

  • Cargas de trabalho de baixa prioridade e tolerantes à latência com transações de longa duração:use a simultaneidade otimista se leituras ou consultas de longa duração puderem atrasar gravações sensíveis à latência. Isso evita atrasos causados por bloqueios de leitura. Por exemplo, transações em clientes móveis com conexões lentas ou transações de baixo SLA que mantêm bloqueios de leitura para muitas linhas ou intervalos grandes.
  • Cargas de trabalho transacionais sensíveis à latência de leitura com baixa disputa de leitura e gravação:em uma configuração multirregional, use a simultaneidade otimista para atender leituras regionalmente, reduzir as latências de leitura e evitar problemas de produção devido ao tráfego de leitura instável para uma divisão ativa. Ele também melhora a disponibilidade de leitura durante sobrecarga ou indisponibilidade do líder.
  • Cargas de trabalho transacionais em que a maioria das transações é somente leitura:mudar para a simultaneidade otimista reduz a latência de confirmação para transações comuns somente leitura nessas cargas de trabalho. Garanta baixa contenção de leitura e gravação para evitar altas taxas de interrupção para transações de leitura e gravação.

Evite usar a simultaneidade otimista em cargas de trabalho transacionais sensíveis à latência em que conflitos de leitura e gravação são frequentes.

Configurar o controle de simultaneidade

É possível usar as bibliotecas de cliente, a API REST e a API RPC do Spanner para especificar o modo de simultaneidade para transações de leitura e gravação.

Bibliotecas de 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

A API REST TransactionOptions do Spanner fornece um enumerador ReadLockMode na mensagem ReadWrite que permite selecionar o modo de bloqueio PESSIMISTIC ou OPTIMISTIC.

RPC

A API RPC Transactionoptions do Spanner fornece um enum ReadLockMode na mensagem ReadWrite que permite selecionar o modo de bloqueio PESSIMISTIC ou OPTIMISTIC.

Drivers

É possível usar os drivers do Spanner para definir read_lock_mode como um parâmetro de conexão no nível da conexão ou como uma opção de instrução SET no nível da transação. Para mais informações sobre cada driver, consulte Visão geral dos drivers.

A seguir