Visão geral das transações

Nesta página, descrevemos as transações no Spanner e apresentamos as interfaces de transação de leitura e gravação, somente leitura e DML particionada do Spanner.

Uma transação no Spanner é um conjunto de leituras e gravações executadas atomicamente em um único ponto lógico no tempo entre colunas, linhas e tabelas de um banco de dados.

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

Tipos de transação

O Spanner é compatível com os seguintes tipos de transação, cada um projetado para padrões específicos de interação de dados:

  • Leitura e gravação:essas transações usam bloqueio pessimista e, se necessário, um commit de duas fases. Elas podem falhar e exigir novas tentativas. Embora estejam confinados a um único banco de dados, eles podem modificar dados em várias tabelas dentro desse banco de dados.

  • Somente leitura:essas transações garantem a consistência dos dados em várias operações de leitura, mas não permitem modificações. Eles são executados em um carimbo de data/hora determinado pelo sistema para consistência ou em um carimbo de data/hora passado configurado pelo usuário. Ao contrário das transações de leitura e gravação, elas não exigem uma operação de confirmação ou bloqueios, embora possam pausar para aguardar a conclusão das operações de gravação em andamento.

  • DML particionada:esse tipo de transação executa instruções DML como operações de DML particionada. Ele é otimizado para atualizações e exclusões de dados em grande escala, como limpeza ou inserção em massa de dados. Para várias gravações que não precisam de uma transação atômica, use gravações em lote. Consulte Modificar dados usando gravações em lote para mais detalhes.

Transações de leitura e gravação

Use transações de leitura e gravação de bloqueio para ler, modificar e gravar dados de maneira atômica em qualquer lugar de um banco de dados. Esse tipo de transação é consistente externamente.

Minimizar o tempo em que uma transação fica ativa. Transações mais curtas aumentam a probabilidade de um commit bem-sucedido e reduzem a disputa. O Spanner tenta manter os bloqueios de leitura ativos enquanto a transação continua realizando leituras e não é encerrada por operações sessions.commit ou sessions.rollback. Se o cliente ficar inativo por longos períodos, o Spanner poderá liberar os bloqueios da transação e interrompê-la.

Conceitualmente, uma transação de leitura/gravação consiste em zero ou mais leituras ou instruções SQL seguidas por sessions.commit. A qualquer momento antes de sessions.commit, o cliente pode enviar uma solicitação sessions.rollback para anular a transação.

Para realizar uma operação de gravação que depende de uma ou mais operações de leitura, use uma transação de leitura e gravação com bloqueio:

  • Se você precisar confirmar uma ou mais operações de gravação de forma atômica, faça essas gravações na mesma transação de leitura e gravação. Por exemplo, se você transferir US$ 200 da conta A para a conta B, execute as duas operações de gravação (diminuindo US $200 da conta A e aumentando US $200 da conta B) e as leituras dos saldos iniciais das contas na mesma transação.
  • Se você quiser dobrar o saldo da conta A, faça as operações de leitura e gravação na mesma transação. Isso garante que o sistema leia o saldo antes de dobrá-lo e atualizá-lo.
  • Se você puder realizar uma ou mais operações de gravação que dependam dos resultados de uma ou mais operações de leitura, faça essas gravações e leituras na mesma transação de leitura e gravação, mesmo que as operações de gravação não sejam executadas. Por exemplo, se você quiser transferir US $200 da conta A para a conta B somente se o saldo atual de A for superior a US $500, inclua a leitura do saldo de A e as operações de gravação condicional 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 somente leitura:

  • Se você estiver apenas realizando operações de leitura e puder expressar a operação usando um método de leitura única, use esse método ou uma transação somente leitura. Ao contrário das transações de leitura e gravação, as leituras únicas não adquirem bloqueios.

Interface

As bibliotecas de cliente do Spanner fornecem uma interface para executar um conjunto de trabalho em uma transação de leitura e gravação, com novas tentativas de cancelamento de transação. Uma transação do Spanner pode exigir várias novas tentativas antes de ser confirmada.

Várias situações podem causar a interrupção de transações. Por exemplo, se duas transações tentarem modificar dados simultaneamente, poderá ocorrer um impasse. Nesses casos, o Spanner cancela uma transação para permitir que a outra continue. Com menos frequência, eventos temporários no Spanner também podem causar cancelamentos de transações.

Como as transações são atômicas, uma transação cancelada não afeta o banco de dados. Tente de novo na mesma sessão para melhorar as taxas de sucesso. Cada nova tentativa que resulta em um erro ABORTED aumenta a prioridade de bloqueio da transação.

Ao usar uma transação em uma biblioteca de cliente do Spanner, você define o corpo da transação como um objeto de função. Essa função encapsula as leituras e gravações realizadas em uma ou mais tabelas de banco de dados. A biblioteca de cliente do Spanner executa essa função repetidamente até que a transação seja confirmada ou encontre um erro que não possa ser repetido.

Exemplo

Suponha que você tenha 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 departamento de marketing pede para você transferir US $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. Use uma transação de leitura e gravação com bloqueio para essa operação porque, dependendo do resultado de uma leitura, é possível que a transação faça alguma gravação.

Veja a seguir como executar uma transação de leitura e gravação:

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;

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 seção descreve a semântica das transações de leitura e gravação no Spanner.

Propriedades

Uma transação de leitura e gravação no Spanner executa um conjunto de leituras e gravações atomicamente. O carimbo de data/hora em que as transações de leitura e gravação são executadas corresponde ao tempo decorrido. A ordem de serialização corresponde à ordem do carimbo de data/hora.

As transações de leitura e gravação oferecem as propriedades ACID dos bancos de dados relacionais. As transações de leitura e gravação do Spanner oferecem propriedades mais fortes do que o ACID típico.

Devido a essas propriedades, como desenvolvedor de aplicativos, você se concentra na exatidão de cada transação por si só, sem se preocupar em como proteger a execução dela contra outras transações que podem ser executadas ao mesmo tempo.

Isolamento para transações de leitura e gravação

Depois de confirmar uma transação que contém uma série de leituras e gravações, você verá o seguinte:

  • A transação retorna valores que refletem um snapshot consistente no carimbo de data/hora de confirmação da transação.
  • Linhas ou intervalos vazios permanecem vazios no momento do commit.
  • A transação confirma todas as gravações no carimbo de data/hora de confirmação da transação.
  • Nenhuma transação pode ver as gravações até que a transação seja confirmada.

Os drivers de cliente do Spanner incluem uma lógica de novas tentativas de transação que mascara erros transitórios ao executar novamente a transação e validar os dados que o cliente observa.

O efeito é que todas as leituras e gravações parecem ter ocorrido em um único momento, tanto da perspectiva da própria transação quanto da perspectiva de outros leitores e gravadores no banco de dados do Spanner. Isso significa que as leituras e gravações ocorrem no mesmo carimbo de data/hora. Para ver um exemplo, consulte Capacidade de serialização e consistência externa.

Isolamento para transações de leitura

Quando uma transação de leitura e gravação executa apenas operações de leitura, ela oferece garantias de consistência semelhantes a uma transação somente leitura. Todas as leituras dentro da transação retornam dados de um carimbo de data/hora consistente, incluindo a confirmação de linhas inexistentes.

Uma diferença é quando uma transação de leitura e gravação é confirmada sem executar uma operação de gravação. Nesse cenário, não há garantia de que os dados lidos na transação permaneceram inalterados no banco de dados entre a operação de leitura e a confirmação da transação.

Para garantir a atualização dos dados e validar que eles não foram modificados desde a última recuperação, é necessária uma leitura subsequente. Essa releitura pode ser realizada em outra transação de leitura e gravação ou com uma leitura consistente.

Para otimizar a eficiência, se uma transação estiver realizando apenas leituras, use uma transação somente leitura em vez de uma transação de leitura/gravação.

Atomicidade, consistência, durabilidade

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

  • Atomicidade. Uma transação é considerada atômica se todas as operações forem concluídas com sucesso ou nenhuma delas. Se alguma operação em uma transação falhar, toda a transação será revertida ao estado original, garantindo a integridade dos dados.
  • Consistência. Uma transação precisa manter a integridade das regras e restrições do banco de dados. Depois que uma transação é concluída, o banco de dados precisa estar em um estado válido, obedecendo a regras predefinidas.
  • Durabilidade. Depois que uma transação é confirmada, as mudanças são armazenadas permanentemente no banco de dados e persistem em caso de falhas no sistema, quedas de energia ou outras interrupções.

Capacidade de serialização e consistência externa

O Spanner oferece garantias transacionais fortes, incluindo capacidade de serialização e consistência externa. Essas propriedades garantem que os dados permaneçam consistentes e que as operações ocorram em uma ordem previsível, mesmo em um ambiente distribuído.

A capacidade de serialização garante que todas as transações pareçam ser executadas uma após a outra em uma única ordem sequencial, mesmo que sejam processadas simultaneamente. O Spanner faz isso atribuindo carimbos de data/hora de confirmação às transações, refletindo a ordem em que elas foram confirmadas.

O Spanner oferece uma garantia ainda mais forte, conhecida como consistência externa. Isso significa que não apenas as transações são confirmadas em uma ordem refletida pelos carimbos de data/hora de confirmação, mas esses carimbos também se alinham ao tempo real. Isso permite comparar carimbos de data/hora de confirmação com o tempo real, fornecendo uma visualização consistente e globalmente ordenada dos seus dados.

Em essência, se uma transação Txn1 for confirmada antes de outra transação Txn2 em tempo real, o carimbo de data/hora de confirmação de Txn1 será anterior ao carimbo de data/hora de confirmação de Txn2.

Veja o exemplo a seguir.

Linha do tempo com a execução de duas transações que leem os mesmos dados

Nesse cenário, durante o período t:

  • A transação Txn1 lê os dados A, prepara uma gravação em A e, em seguida, faz a confirmação.
  • A transação Txn2 começa depois que Txn1 é iniciada. Ele lê os dados B e depois os dados A.

Mesmo que Txn2 tenha começado antes da conclusão de Txn1, Txn2 observa as mudanças feitas por Txn1 em A. Isso ocorre porque Txn2A depois que Txn1 confirma a gravação em A.

Embora Txn1 e Txn2 possam se sobrepor no tempo de execução, os carimbos de data/hora de confirmação, c1 e c2, respectivamente, impõem uma ordem linear de transação. Isso significa:

  • Todas as leituras e gravações em Txn1 parecem ter ocorrido em um único ponto no tempo, c1.
  • Todas as leituras e gravações em Txn2 parecem ter ocorrido em um único ponto no tempo, c2.
  • É importante notar que c1 é anterior a c2 para gravações confirmadas, mesmo que elas tenham ocorrido em máquinas diferentes. Se Txn2 realizar apenas leituras, c1 será anterior ou igual a c2.

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

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

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

Por exemplo, um erro como "Linha não encontrada" ou "Linha já existe" indica que a gravação das mutações armazenadas em buffer encontrou algum erro, por exemplo: uma linha que o cliente está tentando atualizar não existe. Nesse caso, as leituras são garantidas de maneira consistente, as gravações não são aplicadas e a inexistência da linha é garantida para também ser consistente com as leituras.

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

Quando uma transação do Spanner falha, as garantias que você recebe para leituras e gravações 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 armazenadas em buffer. Isso pode acontecer se, por exemplo, uma linha que o cliente está tentando atualizar não existir. Nesses cenários:

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

É possível cancelar operações de leitura assíncronas no Spanner a qualquer momento sem afetar outras operações em andamento na mesma transação. Essa flexibilidade é útil se uma operação de nível superior for cancelada ou se você decidir interromper uma leitura com base nos resultados iniciais.

No entanto, é importante entender que solicitar o cancelamento de uma leitura não garante a rescisão imediata. Depois de um pedido de cancelamento, a operação de leitura ainda pode:

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

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

Desempenho

Esta seção descreve problemas que afetam o desempenho das transações de leitura e gravação.

Controle de simultaneidade de bloqueio

O Spanner permite que vários clientes interajam com o mesmo banco de dados simultaneamente. Para manter a consistência dos dados nessas transações simultâneas, o Spanner tem um mecanismo de bloqueio que usa bloqueios compartilhados e exclusivos.

Quando uma transação executa uma operação de leitura, o Spanner adquire bloqueios compartilhados de leitura nos dados relevantes. Esses bloqueios compartilhados permitem que outras operações de leitura simultâneas acessem os mesmos dados. Essa simultaneidade é mantida até que sua transação se prepare para confirmar as mudanças.

Durante a fase de confirmação, à medida que as gravações são aplicadas, a transação tenta fazer upgrade dos bloqueios para bloqueios exclusivos. Para isso, ele faz o seguinte:

  • Bloqueia novas solicitações de bloqueio compartilhado de leitura nos dados afetados.
  • Aguarde a liberação de todos os bloqueios compartilhados de leitura nos dados.
  • Depois que todos os bloqueios compartilhados de leitura são limpos, ele coloca um bloqueio exclusivo, concedendo acesso único aos dados durante a gravação.

Observações sobre bloqueios:

  • Granularidade:o Spanner aplica bloqueios na granularidade de linha e coluna. Isso significa que, se a transação T1 tiver um bloqueio na coluna A da linha albumid, a transação T2 ainda poderá gravar simultaneamente na coluna B da mesma linha albumid sem conflito.
  • Gravações sem leituras:para gravações sem leituras, o Spanner não exige um bloqueio exclusivo. Em vez disso, ele usa um bloqueio compartilhado de gravação. Isso ocorre porque a ordem de aplicação para gravações sem leituras é determinada pelos carimbos de data/hora de confirmação, permitindo que vários gravadores operem no mesmo item simultaneamente sem conflito. Um bloqueio exclusivo só é necessário se a transação primeiro ler os dados que pretende gravar.
  • Índices secundários para pesquisas de linhas:ao realizar pesquisas de linhas em uma transação de leitura e gravação, o uso de índices secundários pode melhorar significativamente o desempenho. Ao usar índices secundários para limitar as linhas verificadas a um intervalo menor, o Spanner bloqueia menos linhas na tabela, permitindo 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 projetados para consistência de dados no próprio banco de dados do Spanner. Não os use para garantir acesso exclusivo a recursos fora do Spanner. O Spanner pode cancelar transações por vários motivos, incluindo otimizações internas do sistema como movimentação de dados entre recursos de computação. Se uma transação for repetida (explicitamente pelo código do aplicativo ou implicitamente por bibliotecas de cliente, como o driver JDBC do Spanner), os bloqueios só serão mantidos durante a tentativa de confirmação bem-sucedida.
  • Estatísticas de bloqueio:para diagnosticar e investigar conflitos de bloqueio no seu banco de dados, use a ferramenta de introspecção Estatísticas de bloqueio.

Detecção de impasses

O Spanner detecta quando várias transações podem estar bloqueadas e obriga o cancelamento de todas as transações, exceto uma. Considere este cenário: Txn1 mantém um bloqueio no registro A e está aguardando um bloqueio no registro B, enquanto Txn2 mantém um bloqueio no registro B e está aguardando um bloqueio no registro A. Para resolver isso, uma das transações precisa ser cancelada, liberando o bloqueio e permitindo que a outra continue.

O Spanner usa o algoritmo padrão wound-wait para detecção de impasse. Em segundo plano, o Spanner acompanha a idade de cada transação que solicita bloqueios conflitantes. Ele permite que transações mais antigas anulem as mais recentes. Uma transação mais antiga é aquela cuja primeira leitura, consulta ou confirmação ocorreu antes.

Ao priorizar transações mais antigas, o Spanner garante que todas as transações vão adquirir bloqueios com o tempo, depois que tiverem idade suficiente para ter prioridade mais alta. Por exemplo, uma transação mais antiga que precisa de um bloqueio compartilhado de gravador pode interromper uma transação mais recente que tenha um bloqueio compartilhado de leitor.

Execução distribuída

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

Quais tipos de transações podem ser distribuídos? O Spanner pode distribuir a responsabilidade por linhas do banco de dados entre vários servidores. Normalmente, uma linha e as linhas correspondentes da tabela intercalada são disponibilizadas pelo mesmo servidor, assim como duas linhas na mesma tabela com chaves próximas. O Spanner pode executar transações em linhas em diferentes servidores. No entanto, como regra geral, as transações que afetam muitas linhas colocalizadas são mais rápidas e baratas do que aquelas que afetam muitas linhas espalhadas por todo o banco de dados ou uma tabela grande.

As transações mais eficientes no Spanner incluem somente as leituras e gravações que precisam ser aplicadas atomicamente. As transações são mais rápidas quando todas as leituras e gravações acessam dados na mesma parte do espaço de chave.

Transações somente leitura

Além de bloquear transações de leitura e gravação, o Spanner oferece transações somente leitura.

Use uma transação somente leitura quando precisar executar mais de uma leitura no mesmo carimbo de data/hora. Se você puder expressar sua leitura usando um dos métodos de leitura única do Spanner, use esse método. O desempenho da utilização de uma chamada de leitura única é comparável ao de uma única leitura feita em uma transação somente leitura.

Se você estiver lendo uma grande quantidade de dados, use partições para ler os dados em paralelo.

Como as transações somente leitura não gravam, elas não mantêm bloqueios e não bloqueiam outras transações. As transações somente leitura estão de acordo com um prefixo consistente do histórico de confirmações da transação. Portanto, o aplicativo sempre recebe dados consistentes.

Interface

O Spanner oferece uma interface para executar um conjunto de trabalho no contexto de uma transação somente leitura, com novas tentativas para cancelamentos de transações.

Exemplo

O exemplo a seguir mostra como usar uma transação somente leitura para receber dados consistentes para duas leituras no mesmo carimbo de 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 seção descreve a semântica das transações somente leitura.

Transações somente leitura de snapshots

Quando uma transação somente leitura é executada no Spanner, ela realiza todas as leituras em um único ponto lógico no tempo. Isso significa que a transação somente leitura e qualquer outro leitor e gravador simultâneo veem um instantâneo consistente do banco de dados naquele momento específico.

Essas transações somente leitura de snapshot oferecem uma abordagem mais simples para leituras consistentes em comparação com o bloqueio de transações de leitura/gravação. Geralmente, estes são os motivos:

  • Sem bloqueios:as transações somente leitura não adquirem bloqueios. Em vez disso, elas selecionam um carimbo de data/hora do Spanner e executam todas as leituras na versão histórica dos dados. Como elas não usam bloqueios, não bloqueiam transações simultâneas de leitura e gravação.
  • Nenhum encerramento:essas transações nunca são encerradas. Embora possam falhar se o carimbo de data/hora de leitura escolhido for coletado como lixo, a política padrão de coleta de lixo do Spanner geralmente é generosa o suficiente para que a maioria dos aplicativos não encontre esse problema.
  • Sem commits ou rollbacks:as transações somente leitura não exigem chamadas para sessions.commit ou sessions.rollback e são impedidas de fazer isso.

Para executar uma transação de snapshot, o cliente define um limite de carimbo de data/hora, que instrui o Spanner a selecionar um carimbo de data/hora de leitura. Os tipos de limites de carimbo de data/hora incluem:

  • Leituras fortes:garantem que você veja os efeitos de todas as transações confirmadas antes do início da leitura. Todas as linhas em uma única leitura são consistentes. No entanto, as leituras consistentes não são repetíveis, embora elas retornem um carimbo de data/hora, e a leitura novamente nesse mesmo carimbo seja repetível. Duas transações fortes somente leitura consecutivas podem produzir resultados diferentes devido a gravações simultâneas. As consultas em fluxo de alterações precisam usar esse limite. Para mais detalhes, consulte TransactionOptions.ReadOnly.strong.
  • Desatualização exata:essa opção executa leituras em um carimbo de data/hora especificado, seja como um carimbo de data/hora absoluto ou como uma duração de desatualização relativa ao horário atual. Ele garante que você observe um prefixo consistente do histórico de transações globais até esse carimbo de data/hora e bloqueia transações conflitantes que possam ser confirmadas com um carimbo de data/hora menor ou igual ao carimbo de data/hora de leitura. Embora seja um pouco mais rápido que os modos de defasagem limitada, ele pode retornar dados mais antigos. Para mais detalhes, consulte TransactionOptions.ReadOnly.read_timestamp e TransactionOptions.ReadOnly.exact_staleness.
  • Inatividade limitada:o Spanner seleciona o carimbo de data/hora mais recente dentro de um limite de inatividade definido pelo usuário, permitindo a execução na réplica disponível mais próxima sem bloqueio. Todas as linhas retornadas são consistentes. Assim como as leituras consistentes, a inatividade limitada não é repetível, já que leituras diferentes podem ser executadas com carimbos de data/hora diferentes, mesmo com o mesmo limite. Essas leituras operam em duas fases (negociação de carimbo de data/hora e leitura) e geralmente são um pouco mais lentas do que a inatividade exata, mas costumam retornar resultados mais recentes e têm mais chances de serem executadas em uma réplica local. Esse modo só está disponível para transações somente leitura de uso único porque a negociação de carimbo de data/hora exige saber quais linhas serão lidas com antecedência. Para mais detalhes, consulte TransactionOptions.ReadOnly.max_staleness e TransactionOptions.ReadOnly.min_read_timestamp.

Transações de DML particionada

É possível usar a DML particionada para executar instruções UPDATE e DELETE em larga escala sem encontrar limites de transação ou bloquear uma tabela inteira. O Spanner faz isso particionando o espaço de chaves e executando as instruções DML em cada partição em uma transação de leitura e gravação separada.

Para usar DML não particionada, execute instruções em transações de leitura e gravação que você cria explicitamente no código. Para mais detalhes, consulte Como usar a DML.

Interface

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

Exemplos

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

C++

Use a função ExecutePartitionedDml() para executar uma instruçã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#

Use o método ExecutePartitionedUpdateAsync() para executar uma instruçã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

Use o método PartitionedUpdate() para executar uma instruçã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

Use o método executePartitionedUpdate() para executar uma instruçã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

Use o método runPartitionedUpdate() para executar uma instruçã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

Use o método executePartitionedUpdate() para executar uma instruçã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

Use o método execute_partitioned_dml() para executar uma instruçã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

Use o método execute_partitioned_update() para executar uma instruçã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 a seguir exclui 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 seção descreve a semântica da DML particionada.

Noções básicas sobre a execução da DML particionada

É possível executar apenas uma instrução DML particionada por vez, seja usando um método de biblioteca de cliente ou a Google Cloud CLI.

As transações particionadas não são compatíveis com confirmações ou rollbacks. O Spanner executa e aplica a instrução DML imediatamente. Se você cancelar a operação ou ela falhar, o Spanner vai cancelar todas as partições em execução e não vai iniciar as restantes. No entanto, o Spanner não reverter partições que já foram executadas.

Estratégia de aquisição de bloqueio da DML particionada

Para reduzir a disputa por bloqueio, a DML particionada adquire bloqueios de leitura apenas nas linhas que correspondem à cláusula WHERE. Transações menores e independentes usadas para cada partição também mantêm bloqueios por menos tempo.

Limites de transação da sessão

Cada sessão no Spanner pode ter uma transação ativa por vez. Isso inclui leituras e consultas independentes, que usam uma transação internamente e contam para esse limite. Depois que uma transação é concluída, a sessão pode ser reutilizada imediatamente para a próxima transação. Não é necessário criar uma nova sessão para cada transação.

Carimbos de data/hora de leitura antigos e coleta de lixo de versões

O Spanner realiza a coleta de lixo de versões para coletar dados excluídos ou substituídos e recuperar armazenamento. Por padrão, os dados com mais de uma hora são recuperados. O Spanner não pode executar leituras em carimbos de data/hora mais antigos que o VERSION_RETENTION_PERIOD configurado, que é de uma hora por padrão, mas pode ser configurado para até uma semana. Quando as leituras ficam muito antigas durante a execução, elas falham e retornam o erro FAILED_PRECONDITION.

Consultas em fluxo de alterações

Um fluxo de alterações é um objeto de esquema que pode ser configurado para monitorar modificações de dados em um banco de dados inteiro, em tabelas específicas ou em um conjunto definido de colunas em um banco de dados.

Quando você cria um fluxo de alterações, o Spanner define uma função de valor de tabela (TVF) do SQL correspondente. É possível usar essa TVF para consultar os registros de mudança no fluxo de alterações associado com o método sessions.executeStreamingSql. O nome da TVF é gerado com base no nome do fluxo de alterações e sempre começa com READ_.

Todas as consultas em TVFs de fluxo de alterações precisam ser executadas usando a API sessions.executeStreamingSql em uma transação somente leitura de uso único com um timestamp_bound somente leitura forte. Com a TVF de fluxo de alterações, é possível especificar start_timestamp e end_timestamp para o período. Todos os registros de mudança dentro do período de armazenamento podem ser acessados usando o timestamp_bound somente leitura forte. Todos os outros TransactionOptions são inválidos para consultas de fluxo de mudanças.

Além disso, se TransactionOptions.read_only.return_read_timestamp estiver definido como true, a mensagem Transaction que descreve a transação vai retornar um valor especial de 2^63 - 2 em vez de um carimbo de data/hora de leitura válido. Descarte esse valor especial e não o use em consultas subsequentes.

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

Transações inativas

Uma transação é considerada inativa se não tiver leituras ou consultas SQL pendentes e não tiver iniciado uma nos últimos 10 segundos. O Spanner pode anular transações inativas para evitar que elas mantenham bloqueios indefinidamente. Se uma transação inativa for anulada, a confirmação vai falhar e retornar um erro ABORTED. Executar periodicamente uma pequena consulta, como SELECT 1, na transação pode evitar que ela fique inativa.