Vista geral das transações

Esta página descreve as transações no Spanner e apresenta as interfaces de transação DML de leitura/escrita, só de leitura e particionadas do Spanner.

Uma transação no Spanner é um conjunto de leituras e escritas que são executadas atomicamente num único ponto lógico no tempo em colunas, linhas e tabelas numa base de dados.

Uma sessão é usada para realizar transações numa base de dados do Spanner. Uma sessão representa um canal de comunicação lógico com o serviço de base de dados do Spanner. As sessões podem executar uma ou várias transações de cada vez. Para mais informações, consulte o artigo Sessões.

Tipos de transações

O Spanner suporta os seguintes tipos de transações, cada um concebido para padrões de interação de dados específicos:

  • Leitura/escrita: estas transações usam o bloqueio pessimista e, se necessário, uma confirmação de duas fases. Podem falhar e exigir novas tentativas. Embora estejam limitadas a uma única base de dados, podem modificar dados em várias tabelas nessa base de dados.

  • Só de leitura: estas transações garantem a consistência dos dados em várias operações de leitura, mas não permitem modificações de dados. São executados numa data/hora determinada pelo sistema para garantir a consistência ou numa data/hora passada configurada pelo utilizador. Ao contrário das transações de leitura/escrita, não requerem uma operação de confirmação nem bloqueios, embora possam ser pausadas para aguardar a conclusão das operações de escrita em curso.

  • DML particionada: este tipo de transação executa instruções DML como operações de DML particionada. Está otimizado para atualizações e eliminações de dados em grande escala, como a limpeza de dados ou a inserção de dados em massa. Para várias escritas que não precisam de uma transação atómica, considere usar escritas em lote. Consulte o artigo Modifique dados através de gravações em lote para ver detalhes.

Transações de leitura/escrita

Use transações de leitura/escrita de bloqueio para ler, modificar e escrever dados de forma atómica em qualquer lugar numa base de dados. Este tipo de transação é consistente externamente.

Minimizar o tempo durante o qual uma transação está ativa. As durações das transações mais curtas aumentam a probabilidade de uma confirmação bem-sucedida e reduzem a contenção. O Spanner tenta manter os bloqueios de leitura ativos enquanto a transação continuar a executar leituras e a transação não tiver terminado através das operações sessions.commit ou sessions.rollback. Se o cliente permanecer inativo durante longos períodos, o Spanner pode libertar os bloqueios da transação e anular a transação.

Em termos conceptuais, uma transação de leitura/escrita consiste em zero ou mais leituras ou declarações SQL, seguidas de sessions.commit. Em qualquer altura antes de sessions.commit, o cliente pode enviar um pedido sessions.rollback para anular a transação.

Para executar uma operação de escrita que dependa de uma ou mais operações de leitura, use uma transação de leitura/escrita de bloqueio:

  • Se tiver de confirmar uma ou mais operações de escrita de forma atómica, execute essas escritas na mesma transação de leitura/escrita. Por exemplo, se transferir 200 € da conta A para a conta B, execute ambas as operações de escrita (diminuindo a conta A em 200 € e aumentando a conta B em 200 €) e as leituras dos saldos iniciais das contas na mesma transação.
  • Se quiser duplicar o saldo da conta A, execute as operações de leitura e escrita na mesma transação. Isto garante que o sistema lê o saldo antes de o duplicar e, em seguida, o atualiza.
  • Se puder realizar uma ou mais operações de escrita que dependam dos resultados de uma ou mais operações de leitura, realize essas escritas e leituras na mesma transação de leitura/escrita, mesmo que as operações de escrita não sejam executadas. Por exemplo, se quiser transferir 200 € da conta A para a conta B apenas se o saldo atual de A for superior a 500 €, inclua a leitura do saldo de A e as operações de escrita condicionais na mesma transação, mesmo que a transferência não ocorra.

Para realizar operações de leitura, use um único método de leitura ou uma transação só de leitura:

  • Se estiver a realizar apenas operações de leitura e puder expressar a operação de leitura através de um único método de leitura, use esse método de leitura único ou uma transação só de leitura. Ao contrário das transações de leitura/escrita, as leituras únicas não adquirem bloqueios.

Interface

As bibliotecas cliente do Spanner fornecem uma interface para executar um conjunto de tarefas numa transação de leitura/escrita, com novas tentativas para anulações de transações. Uma transação do Spanner pode exigir várias novas tentativas antes de ser confirmada.

Várias situações podem causar a anulação de transações. Por exemplo, se duas transações tentarem modificar dados em simultâneo, pode ocorrer um impasse. Nestes casos, o Spanner anula uma transação para permitir que a outra continue. Com menos frequência, os eventos transitórios no Spanner também podem causar a anulação de transações.

Uma vez que as transações são atómicas, uma transação anulada não afeta a base de dados. Volte a tentar a transação na mesma sessão para melhorar as taxas de êxito. Cada nova tentativa que resulte num erro ABORTED aumenta a prioridade de bloqueio da transação.

Quando usa uma transação numa biblioteca de cliente do Spanner, define o corpo da transação como um objeto de função. Esta função encapsula as leituras e as escritas realizadas numa ou mais tabelas de base de dados. A biblioteca de cliente do Spanner executa esta função repetidamente até que a transação seja confirmada com êxito ou encontre um erro que não possa ser repetido.

Exemplo

Suponhamos que tem uma coluna MarketingBudget na tabela Albums:

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  MarketingBudget INT64
) PRIMARY KEY (SingerId, AlbumId);

O seu departamento de marketing pede-lhe que transfira 200 000 € do orçamento de Albums (2, 2) para Albums (1, 1), mas apenas se o dinheiro estiver disponível no orçamento desse álbum. Deve usar uma transação de leitura/escrita de bloqueio para esta operação, porque a transação pode realizar escritas consoante o resultado de uma leitura.

O exemplo seguinte mostra como executar uma transação de leitura/escrita:

C++

void ReadWriteTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::StatusOr;

  // A helper to read a single album MarketingBudget.
  auto get_current_budget =
      [](spanner::Client client, spanner::Transaction txn,
         std::int64_t singer_id,
         std::int64_t album_id) -> StatusOr<std::int64_t> {
    auto key = spanner::KeySet().AddKey(spanner::MakeKey(singer_id, album_id));
    auto rows = client.Read(std::move(txn), "Albums", std::move(key),
                            {"MarketingBudget"});
    using RowType = std::tuple<std::int64_t>;
    auto row = spanner::GetSingularRow(spanner::StreamOf<RowType>(rows));
    if (!row) return std::move(row).status();
    return std::get<0>(*std::move(row));
  };

  auto commit = client.Commit(
      [&client, &get_current_budget](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        auto b1 = get_current_budget(client, txn, 1, 1);
        if (!b1) return std::move(b1).status();
        auto b2 = get_current_budget(client, txn, 2, 2);
        if (!b2) return std::move(b2).status();
        std::int64_t transfer_amount = 200000;

        return spanner::Mutations{
            spanner::UpdateMutationBuilder(
                "Albums", {"SingerId", "AlbumId", "MarketingBudget"})
                .EmplaceRow(1, 1, *b1 + transfer_amount)
                .EmplaceRow(2, 2, *b2 - transfer_amount)
                .Build()};
      });

  if (!commit) throw std::move(commit).status();
  std::cout << "Transfer was successful [spanner_read_write_transaction]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;
using System.Transactions;

public class ReadWriteWithTransactionAsyncSample
{
    public async Task<int> ReadWriteWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        // This sample transfers 200,000 from the MarketingBudget
        // field of the second Album to the first Album. Make sure to run
        // the Add Column and Write Data To New Column samples first,
        // in that order.

        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        decimal transferAmount = 200000;
        decimal secondBudget = 0;
        decimal firstBudget = 0;

        using var connection = new SpannerConnection(connectionString);
        using var cmdLookup1 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");

        using (var reader = await cmdLookup1.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                // Read the second album's budget.
                secondBudget = reader.GetFieldValue<decimal>("MarketingBudget");
                // Confirm second Album's budget is sufficient and
                // if not raise an exception. Raising an exception
                // will automatically roll back the transaction.
                if (secondBudget < transferAmount)
                {
                    throw new Exception($"The second album's budget {secondBudget} is less than the amount to transfer.");
                }
            }
        }

        // Read the first album's budget.
        using var cmdLookup2 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
        using (var reader = await cmdLookup2.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                firstBudget = reader.GetFieldValue<decimal>("MarketingBudget");
            }
        }

        // Specify update command parameters.
        using var cmdUpdate = connection.CreateUpdateCommand("Albums", new SpannerParameterCollection
        {
            { "SingerId", SpannerDbType.Int64 },
            { "AlbumId", SpannerDbType.Int64 },
            { "MarketingBudget", SpannerDbType.Int64 },
        });

        // Update second album to remove the transfer amount.
        secondBudget -= transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 2;
        cmdUpdate.Parameters["AlbumId"].Value = 2;
        cmdUpdate.Parameters["MarketingBudget"].Value = secondBudget;
        var rowCount = await cmdUpdate.ExecuteNonQueryAsync();

        // Update first album to add the transfer amount.
        firstBudget += transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 1;
        cmdUpdate.Parameters["AlbumId"].Value = 1;
        cmdUpdate.Parameters["MarketingBudget"].Value = firstBudget;
        rowCount += await cmdUpdate.ExecuteNonQueryAsync();
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func writeWithTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		getBudget := func(key spanner.Key) (int64, error) {
			row, err := txn.ReadRow(ctx, "Albums", key, []string{"MarketingBudget"})
			if err != nil {
				return 0, err
			}
			var budget int64
			if err := row.Column(0, &budget); err != nil {
				return 0, err
			}
			return budget, nil
		}
		album2Budget, err := getBudget(spanner.Key{2, 2})
		if err != nil {
			return err
		}
		const transferAmt = 200000
		if album2Budget >= transferAmt {
			album1Budget, err := getBudget(spanner.Key{1, 1})
			if err != nil {
				return err
			}
			album1Budget += transferAmt
			album2Budget -= transferAmt
			cols := []string{"SingerId", "AlbumId", "MarketingBudget"}
			txn.BufferWrite([]*spanner.Mutation{
				spanner.Update("Albums", cols, []interface{}{1, 1, album1Budget}),
				spanner.Update("Albums", cols, []interface{}{2, 2, album2Budget}),
			})
			fmt.Fprintf(w, "Moved %d from Album2's MarketingBudget to Album1's.", transferAmt)
		}
		return nil
	})
	return err
}

Java

static void writeWithTransaction(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        Struct row =
            transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
        long album2Budget = row.getLong(0);
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          long album1Budget =
              transaction
                  .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                  .getLong(0);
          album1Budget += transfer;
          album2Budget -= transfer;
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(1)
                  .set("AlbumId")
                  .to(1)
                  .set("MarketingBudget")
                  .to(album1Budget)
                  .build());
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(2)
                  .set("AlbumId")
                  .to(2)
                  .set("MarketingBudget")
                  .to(album2Budget)
                  .build());
        }
        return null;
      });
}

Node.js

// This sample transfers 200,000 from the MarketingBudget field
// of the second Album to the first Album, as long as the second
// Album has enough money in its budget. Make sure to run the
// addColumn and updateData samples first (in that order).

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const transferAmount = 200000;

// Note: the `runTransaction()` method is non blocking and returns "void".
// For sequential execution of the transaction use `runTransactionAsync()` method which returns a promise.
// For example: await database.runTransactionAsync(async (err, transaction) => { ... })
database.runTransaction(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  let firstBudget, secondBudget;
  const queryOne = {
    columns: ['MarketingBudget'],
    keys: [[2, 2]], // SingerId: 2, AlbumId: 2
  };

  const queryTwo = {
    columns: ['MarketingBudget'],
    keys: [[1, 1]], // SingerId: 1, AlbumId: 1
  };

  Promise.all([
    // Reads the second album's budget
    transaction.read('Albums', queryOne).then(results => {
      // Gets second album's budget
      const rows = results[0].map(row => row.toJSON());
      secondBudget = rows[0].MarketingBudget;
      console.log(`The second album's marketing budget: ${secondBudget}`);

      // Makes sure the second album's budget is large enough
      if (secondBudget < transferAmount) {
        throw new Error(
          `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`,
        );
      }
    }),

    // Reads the first album's budget
    transaction.read('Albums', queryTwo).then(results => {
      // Gets first album's budget
      const rows = results[0].map(row => row.toJSON());
      firstBudget = rows[0].MarketingBudget;
      console.log(`The first album's marketing budget: ${firstBudget}`);
    }),
  ])
    .then(() => {
      console.log(firstBudget, secondBudget);
      // Transfers the budgets between the albums
      firstBudget += transferAmount;
      secondBudget -= transferAmount;

      console.log(firstBudget, secondBudget);

      // Updates the database
      // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
      // must be converted (back) to strings before being inserted as INT64s.
      transaction.update('Albums', [
        {
          SingerId: '1',
          AlbumId: '1',
          MarketingBudget: firstBudget.toString(),
        },
        {
          SingerId: '2',
          AlbumId: '2',
          MarketingBudget: secondBudget.toString(),
        },
      ]);
    })
    .then(() => {
      // Commits the transaction and send the changes to the database
      return transaction.commit();
    })
    .then(() => {
      console.log(
        `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`,
      );
    })
    .catch(err => {
      console.error('ERROR:', err);
    })
    .then(() => {
      transaction.end();
      // Closes the database when finished
      return database.close();
    });
});

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use UnexpectedValueException;

/**
 * Performs a read-write transaction to update two sample records in the
 * database.
 *
 * This will transfer 200,000 from the `MarketingBudget` field for the second
 * Album to the first Album. If the `MarketingBudget` for the second Album is
 * too low, it will raise an exception.
 *
 * Before running this sample, you will need to run the `update_data` sample
 * to populate the fields.
 * Example:
 * ```
 * read_write_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_write_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) use ($spanner) {
        $transferAmount = 200000;

        // Read the second album's budget.
        $secondAlbumKey = [2, 2];
        $secondAlbumKeySet = $spanner->keySet(['keys' => [$secondAlbumKey]]);
        $secondAlbumResult = $t->read(
            'Albums',
            $secondAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        $firstRow = $secondAlbumResult->rows()->current();
        $secondAlbumBudget = $firstRow['MarketingBudget'];
        if ($secondAlbumBudget < $transferAmount) {
            // Throwing an exception will automatically roll back the transaction.
            throw new UnexpectedValueException(
                'The second album\'s budget is lower than the transfer amount: ' . $transferAmount
            );
        }

        $firstAlbumKey = [1, 1];
        $firstAlbumKeySet = $spanner->keySet(['keys' => [$firstAlbumKey]]);
        $firstAlbumResult = $t->read(
            'Albums',
            $firstAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        // Read the first album's budget.
        $firstRow = $firstAlbumResult->rows()->current();
        $firstAlbumBudget = $firstRow['MarketingBudget'];

        // Update the budgets.
        $secondAlbumBudget -= $transferAmount;
        $firstAlbumBudget += $transferAmount;
        printf('Setting first album\'s budget to %s and the second album\'s ' .
            'budget to %s.' . PHP_EOL, $firstAlbumBudget, $secondAlbumBudget);

        // Update the rows.
        $t->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => $firstAlbumBudget],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => $secondAlbumBudget],
        ]);

        // Commit the transaction!
        $t->commit();

        print('Transaction complete.' . PHP_EOL);
    });
}

Python

def read_write_transaction(instance_id, database_id):
    """Performs a read-write transaction to update two sample records in the
    database.

    This will transfer 200,000 from the `MarketingBudget` field for the second
    Album to the first Album. If the `MarketingBudget` is too low, it will
    raise an exception.

    Before running this sample, you will need to run the `update_data` sample
    to populate the fields.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    def update_albums(transaction):
        # Read the second album budget.
        second_album_keyset = spanner.KeySet(keys=[(2, 2)])
        second_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=second_album_keyset,
            limit=1,
        )
        second_album_row = list(second_album_result)[0]
        second_album_budget = second_album_row[0]

        transfer_amount = 200000

        if second_album_budget < transfer_amount:
            # Raising an exception will automatically roll back the
            # transaction.
            raise ValueError("The second album doesn't have enough funds to transfer")

        # Read the first album's budget.
        first_album_keyset = spanner.KeySet(keys=[(1, 1)])
        first_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=first_album_keyset,
            limit=1,
        )
        first_album_row = list(first_album_result)[0]
        first_album_budget = first_album_row[0]

        # Update the budgets.
        second_album_budget -= transfer_amount
        first_album_budget += transfer_amount
        print(
            "Setting first album's budget to {} and the second album's "
            "budget to {}.".format(first_album_budget, second_album_budget)
        )

        # Update the rows.
        transaction.update(
            table="Albums",
            columns=("SingerId", "AlbumId", "MarketingBudget"),
            values=[(1, 1, first_album_budget), (2, 2, second_album_budget)],
        )

    database.run_in_transaction(update_albums)

    print("Transaction complete.")

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner         = Google::Cloud::Spanner.new project: project_id
client          = spanner.client instance_id, database_id
transfer_amount = 200_000

client.transaction do |transaction|
  first_album  = transaction.read("Albums", [:MarketingBudget], keys: [[1, 1]]).rows.first
  second_album = transaction.read("Albums", [:MarketingBudget], keys: [[2, 2]]).rows.first

  raise "The second album does not have enough funds to transfer" if second_album[:MarketingBudget] < transfer_amount

  new_first_album_budget  = first_album[:MarketingBudget] + transfer_amount
  new_second_album_budget = second_album[:MarketingBudget] - transfer_amount

  transaction.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: new_first_album_budget  },
    { SingerId: 2, AlbumId: 2, MarketingBudget: new_second_album_budget }
  ]
end

puts "Transaction complete"

Semântica

Esta secção descreve a semântica das transações de leitura/escrita no Spanner.

Propriedades

Uma transação de leitura/escrita no Spanner executa um conjunto de leituras e escritas de forma atómica. A data/hora em que as transações de leitura/escrita são executadas corresponde ao tempo decorrido. A ordem de serialização corresponde a esta ordem de data/hora.

As transações de leitura/escrita oferecem as propriedades ACID das bases de dados relacionais. As transações de leitura/escrita do Spanner oferecem propriedades mais fortes do que o ACID típico.

Devido a estas propriedades, como programador de aplicações, pode concentrar-se na correção de cada transação por si só, sem se preocupar com a forma de proteger a respetiva execução de outras transações que possam ser executadas em simultâneo.

Isolamento para transações de leitura/escrita

Depois de confirmar com êxito uma transação que contém uma série de leituras e escritas, vê o seguinte:

  • A transação devolve valores que refletem uma imagem consistente na data/hora de confirmação da transação.
  • As linhas ou os intervalos vazios permanecem vazios no momento da confirmação.
  • A transação confirma todas as escritas na data/hora de confirmação da transação.
  • Nenhuma transação pode ver as gravações até que a transação seja confirmada.

Os controladores de cliente do Spanner incluem uma lógica de repetição de transações que oculta erros transitórios ao executar novamente a transação e validar os dados que o cliente observa.

O efeito é que todas as leituras e escritas parecem ter ocorrido num único ponto no tempo, tanto da perspetiva da própria transação como da perspetiva de outros leitores e escritores para a base de dados do Spanner. Isto significa que as leituras e as escritas ocorrem na mesma data/hora. Para ver um exemplo, consulte o artigo Serialização e consistência externa.

Isolamento para transações de leitura

Quando uma transação de leitura/escrita executa apenas operações de leitura, oferece garantias de consistência semelhantes a uma transação só de leitura. Todas as leituras na transação devolvem dados de uma indicação de tempo consistente, incluindo a confirmação de linhas inexistentes.

Uma diferença ocorre quando uma transação de leitura/escrita é confirmada sem executar uma operação de escrita. Neste cenário, não existe garantia de que os dados lidos na transação tenham permanecido inalterados na base de dados entre a operação de leitura e a confirmação da transação.

Para garantir a atualidade dos dados e validar que os dados não foram modificados desde a última obtenção, é necessária uma leitura subsequente. Esta leitura repetida pode ser realizada numa outra transação de leitura/escrita ou com uma leitura forte.

Para uma eficiência ideal, se uma transação estiver a realizar leituras exclusivamente, use uma transação só de leitura em vez de uma transação de leitura/escrita.

Atomicidade, consistência e durabilidade

Além do isolamento, o Spanner oferece as outras garantias de propriedades ACID:

  • Atomicidade. Uma transação é considerada atómica se todas as respetivas operações forem concluídas com êxito ou nenhuma. Se qualquer operação numa transação falhar, toda a transação é revertida para o estado original, o que garante a integridade dos dados.
  • Consistência. Uma transação tem de manter a integridade das regras e das restrições da base de dados. Após a conclusão de uma transação, a base de dados deve estar num estado válido, em conformidade com as regras predefinidas.
  • Durabilidade. Após a confirmação de uma transação, as respetivas alterações são armazenadas permanentemente na base de dados e mantêm-se em caso de falhas do sistema, cortes de energia ou outras interrupções.

Serialização e consistência externa

O Spanner oferece fortes garantias transacionais, incluindo serialização e consistência externa. Estas propriedades garantem que os dados permanecem consistentes e que as operações ocorrem numa ordem previsível, mesmo num ambiente distribuído.

A serialização garante que todas as transações parecem ser executadas umas após as outras numa única ordem sequencial, mesmo que sejam processadas em simultâneo. O Spanner consegue isto atribuindo carimbos de data/hora de confirmação às transações, o que reflete a ordem em que foram confirmadas.

O Spanner oferece uma garantia ainda mais forte conhecida como consistência externa. Isto significa que não só as transações são confirmadas numa ordem refletida pelas respetivas datas/horas de confirmação, como também estas datas/horas estão alinhadas com a hora do mundo real. Isto permite-lhe comparar as datas/horas de confirmação com o tempo real, o que lhe dá uma vista consistente e ordenada globalmente dos seus dados.

Essencialmente, se uma transação Txn1 for confirmada antes de outra transação Txn2 em tempo real, a data/hora de confirmação de Txn1 é anterior à data/hora de confirmação de Txn2.

Considere o seguinte exemplo:

Linha cronológica que mostra a execução de duas transações que leem os mesmos dados

Neste cenário, durante a linha cronológica t:

  • A transação Txn1 lê dados A, prepara uma gravação em A e, em seguida, é confirmada com êxito.
  • A transação Txn2 começa após o início de Txn1. Lê dados B e, em seguida, lê dados A.

Embora Txn2 tenha começado antes de Txn1 estar concluída, Txn2 observa as alterações feitas por Txn1 a A. Isto deve-se ao facto de Txn2 ler A depois de Txn1 confirmar a gravação em A.

Embora Txn1 e Txn2 possam sobrepor-se no respetivo tempo de execução, as respetivas datas/horas de confirmação, c1 e c2, aplicam uma ordem de transação linear. Isto significa que:

  • Todas as leituras e escritas em Txn1 parecem ter ocorrido num único ponto no tempo, c1.
  • Todas as leituras e escritas em Txn2 parecem ter ocorrido num único ponto no tempo, c2.
  • Fundamentalmente, c1 é anterior a c2 para escritas comprometidas, mesmo que as escritas tenham ocorrido em máquinas diferentes. Se Txn2 só realizar leituras, c1 for anterior ou ocorrer ao mesmo tempo que c2.

Esta ordenação forte significa que, se uma operação de leitura subsequente observar os efeitos de Txn2, também observa os efeitos de Txn1. Esta propriedade é verdadeira para todas as transações confirmadas com êxito.

Garantias de leitura e escrita em caso de falha na transação

Se uma chamada para executar uma transação falhar, as garantias de leitura e escrita que tem dependem do erro com que a chamada de confirmação subjacente falhou.

Por exemplo, um erro como "Linha não encontrada" ou "A linha já existe" significa que a escrita das mutações em buffer encontrou algum erro. Por exemplo, uma linha que o cliente está a tentar atualizar não existe. Nesse caso, as leituras são consistentes garantidamente, as escritas não são aplicadas e a não existência da linha também é garantidamente consistente com as leituras.

Garantias de leitura e escrita em caso de falha na transação

Quando uma transação do Spanner falha, as garantias que recebe para leituras e escritas dependem do erro específico encontrado durante a operação.commit

Por exemplo, uma mensagem de erro como "Linha não encontrada" ou "Linha já existe" indica um problema durante a gravação de mutações em buffer. Isto pode ocorrer se, por exemplo, uma linha que o cliente está a tentar atualizar não existir. Nestes cenários:

  • As leituras são consistentes: todos os dados lidos durante a transação têm a garantia de serem consistentes até ao ponto do erro.
  • As escritas não são aplicadas: as mutações que a transação tentou não são confirmadas na base de dados.
  • Consistência das linhas: a não existência (ou o estado existente) da linha que acionou o erro é consistente com as leituras realizadas na transação.

Pode cancelar operações de leitura assíncronas no Spanner em qualquer altura sem afetar outras operações em curso na mesma transação. Esta flexibilidade é útil se uma operação de nível superior for cancelada ou se decidir anular uma leitura com base nos resultados iniciais.

No entanto, é importante compreender que a solicitação do cancelamento de uma leitura não garante a sua rescisão imediata. Após um pedido de cancelamento, a operação de leitura pode continuar a:

  • Concluído com êxito: a leitura pode terminar o processamento e devolver resultados antes de o cancelamento entrar em vigor.
  • Falha por outro motivo: a leitura pode terminar devido a um erro diferente, como uma anulação.
  • Devolver resultados incompletos: a leitura pode devolver resultados parciais, que são validados como parte do processo de confirmação da transação.

Também vale a pena notar a distinção com as operações de commit de transações: a anulação de um commit anula toda a transação, a menos que a transação já tenha sido confirmada ou falhado por outro motivo.

Desempenho

Esta secção descreve os problemas que afetam o desempenho das transações de leitura/escrita.

Bloquear o controlo de simultaneidade

O Spanner permite que vários clientes interajam com a mesma base de dados em simultâneo. Para manter a consistência dos dados nestas transações concorrentes, o Spanner tem um mecanismo de bloqueio que usa bloqueios partilhados e exclusivos.

Quando uma transação executa uma operação de leitura, o Spanner adquire bloqueios de leitura partilhados nos dados relevantes. Estes bloqueios partilhados permitem que outras operações de leitura simultâneas acedam aos mesmos dados. Esta simultaneidade é mantida até que a sua transação se prepare para confirmar as alterações.

Durante a fase de confirmação, à medida que as escritas são aplicadas, a transação tenta atualizar os respetivos bloqueios para bloqueios exclusivos. Para o conseguir, faz o seguinte:

  • Bloqueia todos os novos pedidos de bloqueio de leitura partilhados nos dados afetados.
  • Aguarda que todos os bloqueios de leitura partilhados existentes nesses dados sejam libertados.
  • Depois de todos os bloqueios de leitura partilhados serem limpos, coloca um bloqueio exclusivo, concedendo-lhe acesso exclusivo aos dados durante a gravação.

Notas sobre os bloqueios:

  • Granularidade: o Spanner aplica bloqueios ao nível da linha e da coluna. Isto significa que, se a transação T1 tiver um bloqueio na coluna A da linha albumid, a transação T2 pode continuar a escrever na coluna B da mesma linha albumid em simultâneo sem conflitos.
  • Escritas sem leituras: para escritas sem leituras, o Spanner não requer um bloqueio exclusivo. Em alternativa, usa um bloqueio partilhado de gravação. Isto acontece porque a ordem de aplicação das escritas sem leituras é determinada pelas respetivas datas/horas de confirmação, o que permite que vários autores operem no mesmo item em simultâneo sem conflitos. Um bloqueio exclusivo só é necessário se a sua transação ler primeiro os dados que pretende escrever.
  • Índices secundários para pesquisas de linhas: quando efetua pesquisas de linhas numa transação de leitura/escrita, a utilização de índices secundários pode melhorar significativamente o desempenho. Ao usar índices secundários para limitar as linhas analisadas a um intervalo mais pequeno, o Spanner bloqueia menos linhas na tabela, o que permite uma maior modificação simultânea de linhas fora desse intervalo específico.
  • Acesso exclusivo a recursos externos: os bloqueios internos do Spanner são concebidos para a consistência dos dados na própria base de dados do Spanner. Não os use para garantir o acesso exclusivo a recursos fora do Spanner. O Spanner pode anular transações por vários motivos, incluindo otimizações internas do sistema, como a movimentação de dados entre recursos de computação. Se uma transação for repetida (explícita ou implicitamente pelo código da aplicação ou pelas bibliotecas de cliente, como o controlador JDBC do Spanner), é garantido que os bloqueios foram mantidos apenas durante a tentativa de confirmação bem-sucedida.
  • Estatísticas de bloqueio: para diagnosticar e investigar conflitos de bloqueio na sua base de dados, pode usar a ferramenta de introspeção Estatísticas de bloqueio.

Deteção de impasse

O Spanner deteta quando várias transações podem estar bloqueadas e força a anulação de todas as transações, exceto uma. Considere este cenário: Txn1 tem um bloqueio no registo A e está à espera de um bloqueio no registo B, enquanto Txn2 tem um bloqueio no registo B e está à espera de um bloqueio no registo A. Para resolver este problema, uma das transações tem de ser anulada, libertando o respetivo bloqueio e permitindo que a outra avance.

O Spanner usa o algoritmo padrão wound-wait para a deteção de deadlock. Nos bastidores, o Spanner acompanha a idade de cada transação que pede bloqueios em conflito. Permite que as transações mais antigas anulem as mais recentes. Uma transação mais antiga é aquela cuja leitura, consulta ou confirmação mais antiga ocorreu mais cedo.

Ao dar prioridade às transações mais antigas, o Spanner garante que todas as transações acabam por adquirir bloqueios depois de ficarem suficientemente antigas para terem uma prioridade mais elevada. Por exemplo, uma transação mais antiga que precise de um bloqueio partilhado pelo escritor pode anular uma transação mais recente que tenha um bloqueio partilhado pelo leitor.

Execução distribuída

O Spanner pode executar transações em dados que abrangem vários servidores, embora esta capacidade tenha um custo de desempenho em comparação com as transações de servidor único.

Que tipos de transações podem ser distribuídos? O Spanner pode distribuir a responsabilidade pelas linhas da base de dados por vários servidores. Normalmente, uma linha e as linhas da tabela intercaladas correspondentes são publicadas pelo mesmo servidor, tal como duas linhas na mesma tabela com chaves próximas. O Spanner pode realizar transações em linhas em servidores diferentes. No entanto, como regra geral, as transações que afetam muitas linhas localizadas são mais rápidas e baratas do que as que afetam muitas linhas dispersas pela base de dados ou uma tabela grande.

As transações mais eficientes no Spanner incluem apenas as leituras e as escritas que devem ser aplicadas de forma atómica. As transações são mais rápidas quando todas as leituras e escritas acedem aos dados na mesma parte do espaço de chaves.

Transações só de leitura

Além de bloquear as transações de leitura/escrita, o Spanner oferece transações só de leitura.

Use uma transação de leitura apenas quando precisar de executar mais do que uma leitura na mesma data/hora. Se puder expressar a sua leitura através de um dos métodos de leitura única do Spanner, deve usar esse método de leitura única. O desempenho da utilização de uma única chamada de leitura deve ser comparável ao desempenho de uma única leitura feita numa transação só de leitura.

Se estiver a ler uma grande quantidade de dados, considere usar partições para ler os dados em paralelo.

Uma vez que as transações só de leitura não escrevem, não mantêm bloqueios nem bloqueiam outras transações. As transações só de leitura observam um prefixo consistente do histórico de confirmações de transações, pelo que a sua aplicação recebe sempre dados consistentes.

Interface

O Spanner fornece uma interface para executar um conjunto de tarefas no contexto de uma transação só de leitura, com novas tentativas para anulações de transações.

Exemplo

O exemplo seguinte mostra como usar uma transação de leitura apenas para obter dados consistentes para duas leituras na mesma data/hora:

C++

void ReadOnlyTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto read_only = spanner::MakeReadOnlyTransaction();

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
  using RowType = std::tuple<std::int64_t, std::int64_t, std::string>;

  // Read#1.
  auto rows1 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 1 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows1)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
  // Read#2. Even if changes occur in-between the reads the transaction ensures
  // that Read #1 and Read #2 return the same data.
  auto rows2 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 2 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows2)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Transactions;

public class QueryDataWithTransactionAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        using var connection = new SpannerConnection(connectionString);

        // Opens the connection so that the Spanner transaction included in the TransactionScope
        // is read-only TimestampBound.Strong.
        await connection.OpenAsync(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: default);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : " + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : " + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : " + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }

        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readOnlyTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	ro := client.ReadOnlyTransaction()
	defer ro.Close()
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := ro.Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}

	iter = ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"SingerId", "AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}
}

Java

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    try (ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
      while (queryResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
      }
    } // queryResultSet.close() is automatically called here
    try (ResultSet readResultSet =
        transaction.read(
          "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    } // readResultSet.close() is automatically called here
  } // transaction.close() is automatically called here
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Gets a transaction object that captures the database state
// at a specific point in time
database.getSnapshot(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

  try {
    // Read #1, using SQL
    const [qOneRows] = await transaction.run(queryOne);

    qOneRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    const queryTwo = {
      columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
    };

    // Read #2, using the `read` method. Even if changes occur
    // in-between the reads, the transaction ensures that both
    // return the same data.
    const [qTwoRows] = await transaction.read('Albums', queryTwo);

    qTwoRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    console.log('Successfully executed read-only transaction.');
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    transaction.end();
    // Close the database when finished.
    await database.close();
  }
});

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads data inside of a read-only transaction.
 *
 * Within the read-only transaction, or "snapshot", the application sees
 * consistent view of the database at a particular timestamp.
 * Example:
 * ```
 * read_only_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_only_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $snapshot = $database->snapshot();
    $results = $snapshot->execute(
        'SELECT SingerId, AlbumId, AlbumTitle FROM Albums'
    );
    print('Results from the first read:' . PHP_EOL);
    foreach ($results as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }

    // Perform another read using the `read` method. Even if the data
    // is updated in-between the reads, the snapshot ensures that both
    // return the same data.
    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['SingerId', 'AlbumId', 'AlbumTitle']
    );

    print('Results from the second read:' . PHP_EOL);
    foreach ($results->rows() as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_only_transaction(instance_id, database_id):
    """Reads data inside of a read-only transaction.

    Within the read-only transaction, or "snapshot", the application sees
    consistent view of the database at a particular timestamp.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot(multi_use=True) as snapshot:
        # Read using SQL.
        results = snapshot.execute_sql(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums"
        )

        print("Results from first read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

        # Perform another read using the `read` method. Even if the data
        # is updated in-between the reads, the snapshot ensures that both
        # return the same data.
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), keyset=keyset
        )

        print("Results from second read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

client.snapshot do |snapshot|
  snapshot.execute("SELECT SingerId, AlbumId, AlbumTitle FROM Albums").rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end

  # Even if changes occur in-between the reads, the transaction ensures that
  # both return the same data.
  snapshot.read("Albums", [:AlbumId, :AlbumTitle, :SingerId]).rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end
end

Semântica

Esta secção descreve a semântica das transações só de leitura.

Transações só de leitura do instantâneo

Quando uma transação só de leitura é executada no Spanner, executa todas as leituras num único ponto lógico no tempo. Isto significa que tanto a transação de leitura exclusiva como quaisquer outros leitores e escritores simultâneos veem uma imagem consistente da base de dados nesse momento específico.

Estas transações de leitura só de leitura instantâneas oferecem uma abordagem mais simples para leituras consistentes em comparação com o bloqueio de transações de leitura/escrita. Veja o motivo:

  • Sem bloqueios: as transações só de leitura não adquirem bloqueios. Em vez disso, funcionam selecionando uma data/hora do Spanner e executando todas as leituras em relação a essa versão histórica dos dados. Como não usam bloqueios, não bloqueiam transações de leitura/escrita simultâneas.
  • Sem anulações: estas transações nunca são anuladas. Embora possam falhar se a data/hora de leitura escolhida for recolhida como lixo, a política de recolha de lixo predefinida do Spanner é normalmente suficientemente generosa para que a maioria das aplicações não encontre este problema.
  • Sem compromissos nem reversões: as transações só de leitura não requerem chamadas para sessions.commit ou sessions.rollback e, na verdade, são impedidas de o fazer.

Para executar uma transação de instantâneo, o cliente define um limite de data/hora, que indica ao Spanner como selecionar uma data/hora de leitura. Os tipos de limites de data/hora incluem o seguinte:

  • Leituras fortes: estas leituras garantem que vê os efeitos de todas as transações confirmadas antes do início da leitura. Todas as linhas numa única leitura são consistentes. No entanto, as leituras fortes não são repetíveis, embora as leituras fortes devolvam uma data/hora, e a leitura novamente nessa mesma data/hora seja repetível. Duas transações fortes só de leitura consecutivas podem produzir resultados diferentes devido a escritas simultâneas. As consultas em streams de alterações têm de usar este limite. Para mais detalhes, consulte TransactionOptions.ReadOnly.strong.
  • Obsolecência exata: esta opção executa leituras numa data/hora especificada por si, seja como uma data/hora absoluta ou como uma duração de obsolecência relativa à hora atual. Garante que observa um prefixo consistente do histórico de transações global até essa data/hora e bloqueia transações em conflito que possam ser confirmadas com uma data/hora inferior ou igual à data/hora de leitura. Embora seja ligeiramente mais rápido do que os modos de desatualização limitada, pode devolver dados mais antigos. Para mais detalhes, consulte TransactionOptions.ReadOnly.read_timestamp e TransactionOptions.ReadOnly.exact_staleness.
  • Obsolecência limitada: o Spanner seleciona a data/hora mais recente dentro de um limite de obsolecência definido pelo utilizador, o que permite a execução na réplica disponível mais próxima sem bloqueio. Todas as linhas devolvidas são consistentes. À semelhança das leituras fortes, a desatualização limitada não é repetível, uma vez que diferentes leituras podem ser executadas em diferentes datas/horas, mesmo com o mesmo limite. Estas leituras funcionam em duas fases (negociação da data/hora e, em seguida, leitura) e são normalmente ligeiramente mais lentas do que a desatualização exata, mas devolvem frequentemente resultados mais recentes e têm maior probabilidade de serem executadas numa réplica local. Este modo só está disponível para transações só de leitura de utilização única porque a negociação da data/hora requer saber que linhas vão ser lidas antecipadamente. Para mais detalhes, consulteTransactionOptions.ReadOnly.max_staleness e TransactionOptions.ReadOnly.min_read_timestamp.

Transações DML particionadas

Pode usar o DML particionado para executar declarações UPDATE e DELETE em grande escala sem encontrar limites de transação nem bloquear uma tabela inteira. O Spanner consegue isto ao dividir o espaço de chaves e executar as declarações DML em cada partição numa transação de leitura/escrita separada.

Para usar DML não particionado, executa declarações em transações de leitura/escrita que cria explicitamente no seu código. Para mais detalhes, consulte o artigo Usar DML.

Interface

O Spanner fornece a interface TransactionOptions.partitionedDml para executar uma única instrução DML particionada.

Exemplos

O exemplo de código seguinte atualiza a coluna MarketingBudget da tabela Albums.

C++

Use a função ExecutePartitionedDml() para executar uma declaração DML particionada.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#

Usa o método ExecutePartitionedUpdateAsync() para executar uma declaração DML particionada.


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go

Usa o método PartitionedUpdate() para executar uma declaração DML particionada.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

Usa o método executePartitionedUpdate() para executar uma declaração DML particionada.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

Usa o método runPartitionedUpdate() para executar uma declaração DML particionada.

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

Usa o método executePartitionedUpdate() para executar uma declaração DML particionada.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

Usa o método execute_partitioned_dml() para executar uma declaração DML particionada.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

Usa o método execute_partitioned_update() para executar uma declaração DML particionada.

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

O exemplo de código seguinte elimina linhas da tabela Singers com base na coluna SingerId.

C++

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

print("{} record(s) deleted.".format(row_ct))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."

Semântica

Esta secção descreve a semântica para DML particionada.

Compreender a execução de DML particionada

Só pode executar uma declaração DML particionada de cada vez, quer esteja a usar um método da biblioteca de cliente ou a Google Cloud CLI.

As transações particionadas não suportam commits nem reversões. O Spanner executa e aplica a declaração DML imediatamente. Se cancelar a operação ou a operação falhar, o Spanner cancela todas as partições em execução e não inicia as restantes. No entanto, o Spanner não reverte nenhuma partição que já tenha sido executada.

Estratégia de aquisição de bloqueios DML particionada

Para reduzir a contenção de bloqueios, o DML particionado adquire bloqueios de leitura apenas em linhas que correspondam à cláusula WHERE. As transações mais pequenas e independentes usadas para cada partição também mantêm os bloqueios durante menos tempo.

Limites de transação da sessão

Cada sessão no Spanner pode ter uma transação ativa de cada vez. Isto inclui leituras e consultas autónomas, que usam internamente uma transação e contam para este limite. Após a conclusão de uma transação, a sessão pode ser imediatamente reutilizada para a transação seguinte. Não é necessário criar uma nova sessão para cada transação.

Datas/horas de leitura antigas e recolha de lixo de versões

O Spanner realiza a recolha de lixo de versões para recolher dados eliminados ou substituídos e reclamar armazenamento. Por predefinição, os dados com mais de uma hora são reclamados. O Spanner não pode executar leituras em datas/horas anteriores ao VERSION_RETENTION_PERIOD configurado, que é de uma hora por predefinição, mas pode ser configurado até uma semana. Quando as leituras ficam demasiado antigas durante a execução, falham e devolvem o erro FAILED_PRECONDITION.

Consultas sobre streams de alterações

Uma stream de alterações é um objeto de esquema que pode configurar para monitorizar modificações de dados numa base de dados inteira, em tabelas específicas ou num conjunto definido de colunas numa base de dados.

Quando cria uma stream de alterações, o Spanner define uma função com valor de tabela (TVF) SQL correspondente. Pode usar esta TVF para consultar os registos de alterações no fluxo de alterações associado com o método sessions.executeStreamingSql. O nome do TVF é gerado a partir do nome da stream de alterações e começa sempre com READ_.

Todas as consultas em TVFs de fluxo de alterações têm de ser executadas através da API sessions.executeStreamingSql numa transação só de leitura de utilização única timestamp_bound. O TVF de stream de alterações permite-lhe especificar start_timestamp e end_timestamp para o intervalo de tempo. Todos os registos de alterações dentro do período de retenção são acessíveis através desta leitura forte só de leitura timestamp_bound. Todas as outras TransactionOptions são inválidas para consultas de fluxo de alterações.

Além disso, se TransactionOptions.read_only.return_read_timestamp estiver definido como true, a mensagem Transaction que descreve a transação devolve um valor especial de 2^63 - 2 em vez de uma data/hora de leitura válida. Deve rejeitar este valor especial e não o usar para consultas subsequentes.

Para mais informações, consulte o artigo Fluxo de trabalho de consulta de streams de alterações.

Transações inativas

Uma transação é considerada inativa se não tiver leituras pendentes nem consultas SQL e não tiver iniciado nenhuma nos últimos 10 segundos. O Spanner pode anular transações inativas para impedir que mantenham bloqueios indefinidamente. Se uma transação inativa for anulada, a confirmação falha e devolve um erro ABORTED. A execução periódica de uma pequena consulta, como SELECT 1, na transação pode impedir que esta fique inativa.