Esta página explica as transações no Spanner e inclui um exemplo de código para executar transações.
Introdução
Uma transação no Spanner é um conjunto de leituras e gravações executadas atomicamente em um único ponto lógico no tempo em colunas, linhas e tabelas de um banco de dados.
O Spanner é compatível com estes modos de transação:
Leitura e gravação com bloqueio. Essas transações dependem de bloqueio pessimista e, se necessário, confirmação em duas fases. O bloqueio de transações de leitura e gravação pode ser interrompido, exigindo uma nova tentativa do aplicativo.
Somente leitura. Esse tipo de transação oferece consistência garantida em várias leituras, mas não permite gravações. Por padrão, as transações somente leitura são executadas em um carimbo de data/hora escolhido pelo sistema que garante a consistência externa, mas elas também podem ser configuradas para leitura em um carimbo de data/hora no passado. As transações somente leitura não precisam ser confirmadas e não são bloqueadas. Além disso, as transações somente leitura podem esperar que as gravações em andamento sejam concluídas antes de serem executadas.
DML particionada. Esse tipo de transação executa uma instrução de linguagem de manipulação de dados (DML) como DML particionada. A DML particionada foi projetada para atualizações e exclusões em massa, principalmente limpeza e preenchimento periódicos. Se você precisar confirmar um grande número de gravações cegas, mas não precisar de uma transação atômica, modifique em massa suas tabelas do Spanner usando a gravação em lote. Para mais informações, consulte Modificar dados usando gravações em lote.
Esta página descreve as propriedades gerais e a semântica das transações no Spanner. Além disso, apresenta as interfaces de transações de leitura e gravação, somente leitura e DML particionada no Spanner.
Transações de leitura e gravação
Estes são os cenários em que você precisa usar uma transação de leitura e gravação com bloqueio:
- Se você fizer uma gravação que dependa do resultado de uma ou mais leituras, faça a gravação e as leituras na mesma transação de leitura e gravação.
- Exemplo: dobrar o saldo da conta bancária A. A leitura do saldo de A precisa estar na mesma transação que a gravação para substituir o saldo pelo valor duplicado.
- Se você fizer uma ou mais gravações que precisam ser confirmadas atomicamente, faça-as na mesma transação de leitura e gravação.
- Exemplo: transferir US$ 200 da conta A para a conta B. As duas gravações (uma para subtrair US$ 200 de A e outra para adicionar US$ 200 a B) e as leituras dos saldos das contas iniciais precisam estar na mesma transação.
- Se você pode fazer uma ou mais gravações, dependendo dos resultados de uma ou mais leituras, faça as gravações e as leituras na mesma transação de leitura e gravação, mesmo que as gravações acabem não sendo executadas.
- Exemplo: transferir US$ 200 da conta bancária A para a conta bancária B se o saldo atual de A for superior a US$ 500. A transação precisa ter uma leitura do saldo de A e uma instrução condicional que contenha as gravações.
Neste cenário, não use uma transação de leitura e gravação com bloqueio:
- Se você está apenas fazendo leituras e pode expressá-las usando um método de leitura única, use esse método ou uma transação somente leitura. As leituras únicas não são bloqueadas, ao contrário das transações de leitura e gravação.
Propriedades
Uma transação de leitura e gravação no Spanner executa um conjunto de leituras e gravações atomicamente em um único ponto lógico no tempo. Além disso, o carimbo de data/hora em que as transações de leitura e gravação são executadas corresponde à hora de um relógio normal, e a ordem de serialização corresponde à ordem do carimbo de data/hora.
Por que usar uma transação de leitura e gravação? Essas transações oferecem as propriedades ACID dos bancos de dados relacionais. Na realidade, as transações de leitura e gravação do Spanner oferecem garantias ainda mais fortes do que o ACID tradicional. Consulte a seção Semântica abaixo.
Isolamento
Confira a seguir as propriedades de isolamento para transações de leitura-gravação e somente leitura.
Transações que leem e gravam
Estas são as propriedades de isolamento que você recebe após confirmar uma transação que contém uma série de leituras (ou consultas) e gravações:
- Todas as leituras na transação retornam valores que refletem um snapshot consistente feito no carimbo de data/hora da transação.
- As linhas ou intervalos vazios permaneceram assim no momento da confirmação.
- Todas as gravações na transação foram confirmadas no carimbo de data/hora de confirmação da transação.
- As gravações não eram visíveis para nenhuma transação até que ela fosse confirmada.
Alguns drivers de cliente do Spanner contêm lógica de nova tentativa de transação para mascarar erros temporários, o que é feito executando a transação novamente e validando os dados observados pelo cliente.
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. Em outras palavras, as leituras e gravações acabam ocorrendo no mesmo carimbo de data/hora. Veja uma ilustração disso na seção Consistência externa e capacidade de serialização abaixo.
Transações que só leem
As garantias para uma transação de leitura e gravação que somente lê são similares: todas as leituras dentro dessa transação retornam dados do mesmo carimbo de data/hora, mesmo para a inexistência de linhas. Uma diferença é que, se você lê dados e depois confirma a transação de leitura e gravação sem nenhuma gravação, não há garantia de que os dados não mudaram no banco de dados após a leitura e antes da confirmação. Se você quer saber se os dados foram alterados desde a última leitura, a melhor abordagem é lê-los novamente (em uma transação de leitura/gravação ou usando uma leitura forte). Por questões de eficiência, e se você já sabe que vai apenas ler e não gravar, use uma transação somente leitura em vez de uma transação de leitura/gravação.
Atomicidade, consistência, durabilidade
Além da propriedade de isolamento, o Spanner fornece atomicidade (se qualquer uma das gravações na transação for confirmada, todas serão confirmadas), consistência (o banco de dados permanece em um estado consistente após a transação) e durabilidade (os dados confirmados permanecem confirmados).
Benefícios dessas propriedades
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.
Interface
As bibliotecas de cliente do Spanner fornecem uma interface para executar um corpo de trabalho no contexto de uma transação de leitura e gravação, com novas tentativas de cancelamento de transações. Confira um cenário resumido: pode ser necessário testar uma transação do Spanner várias vezes antes da confirmação. Por exemplo, se duas transações tentam trabalhar em dados ao mesmo tempo de uma maneira que possa causar impasse, o Spanner aborta uma delas para que a outra possa progredir. Mais raramente, eventos temporários no Spanner podem resultar no cancelamento de algumas transações. Como as transações são atômicas, uma transação cancelada não tem efeito visível no banco de dados. Portanto, execute as transações com novas tentativas até que tenham sucesso.
Ao usar uma transação em uma biblioteca de cliente do Spanner, você define o corpo de uma transação (ou seja, as leituras e gravações a serem executadas em uma ou mais tabelas em um banco de dados) na forma de um objeto de função. Em segundo plano, a biblioteca de cliente do Spanner executa a função repetidamente até que a transação seja confirmada ou um erro não repetível seja encontrado.
Exemplo
Imagine que você adicionou uma coluna MarketingBudget
à tabela Albums
mostrada na página "Modelo de dados e esquema":
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
O departamento de marketing decidiu promover o álbum codificado por Albums (1, 1)
e pediu para você transferir US$ 200.000 do orçamento de Albums
(2, 2)
, 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++
C#
Go
Java
Node.js
PHP
Python
Ruby
Semântica
Capacidade de serialização e consistência externa
O Spanner oferece "capacidade de serialização", o que significa que todas as transações aparecem como se tivessem sido executadas em uma ordem serial, mesmo que algumas das leituras, gravações e outras operações de transações distintas realmente tenham ocorrido em paralelo. O Spanner atribui carimbos de data/hora de confirmação que refletem a ordem das transações confirmadas para implementar essa propriedade. Na verdade, o Spanner oferece uma garantia mais forte do que a capacidade de serialização chamada consistência externa: as transações são confirmadas em uma ordem que é refletida nos carimbos de data/hora de confirmação delas, e eles refletem o tempo real para que você possa compará-los ao horário no relógio. As leituras em uma transação veem tudo que foi confirmado antes disso, e as gravações são vistas por tudo que começar depois da confirmação da transação.
Por exemplo, veja a execução de duas transações ilustrada no diagrama abaixo:
A transação Txn1
em azul lê alguns dados A
, armazena em buffer uma gravação em A
e, em seguida, faz a confirmação. A transação Txn2
em verde inicia após Txn1
, lê alguns dados B
e, em seguida, lê os dados A
. Como Txn2
lê o valor de A
depois que Txn1
confirmou a gravação para A
, Txn2
verá o efeito da gravação de Txn1
em A
, mesmo que Txn2
tenha começado antes de Txn1
terminar.
Mesmo que haja alguma sobreposição na hora em que Txn1
e Txn2
estão sendo executadas, os carimbos de data/hora de confirmação c1
e c2
respeitam uma ordem de transação linear. Isso significa que todos os efeitos das leituras e gravações de Txn1
parecem ter ocorrido em um único ponto de tempo (c1
) e todos os efeitos das leituras e gravações Txn2
também parecem ter ocorrido em um único ponto de tempo (c2
). Além disso, c1 < c2
(que é garantido porque as gravações confirmadas de Txn1
e Txn2
confirmaram a gravação, o que é verdade mesmo se as gravações aconteceram em máquinas diferentes), o que respeita a ordem de Txn1
acontecer antes de Txn2
.
No entanto, se Txn2
apenas fez leituras na transação, então c1 <= c2
.
Nas leituras, um prefixo do histórico de confirmação é considerado: se uma leitura vê o efeito de Txn2
, ela também percebe o efeito de Txn1
. Todas as transações confirmadas com sucesso têm essa propriedade.
Garantias de leitura e gravaçã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 da 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.
Como cancelar operações da transação
As operações de leitura assíncronas podem ser canceladas a qualquer momento pelo usuário, por exemplo, quando uma operação de nível superior é cancelada ou você decide parar uma leitura com base nos resultados iniciais recebidos da leitura. Isso não afeta outras operações dentro da transação.
No entanto, mesmo que você tente cancelar a leitura, o Spanner não garante que isso aconteça. Depois de solicitar o cancelamento de uma leitura, ela ainda pode ser concluída ou falhar por algum outro motivo (por exemplo, anulação). Além disso, essa leitura cancelada pode realmente retornar alguns resultados para você. Esses resultados, possivelmente incompletos, serão validados como parte da confirmação da transação.
Observe que, ao contrário das leituras, o cancelamento de uma operação de confirmação da transação resultará na anulação da transação, a menos que ela já tenha sido confirmada ou falhado por outro motivo.
Desempenho
Bloqueio
O Spanner permite que vários clientes interajam simultaneamente com o mesmo banco de dados. Para garantir a consistência de várias transações simultâneas, o Spanner usa uma combinação de bloqueios compartilhados e exclusivos para controlar o acesso aos dados. Quando você executa uma leitura como parte de uma transação, o Spanner adquire bloqueios compartilhados de leitura. Isso permite que outras leituras continuem acessando os dados até que a transação esteja pronta para confirmação. No momento da confirmação da transação e da aplicação das gravações, a transação tenta fazer upgrade para um bloqueio exclusivo. Ele impede novos bloqueios compartilhados de leitura nos dados e espera que os atuais sejam limpos. Em seguida, coloca um bloqueio único para acesso exclusivo aos dados.
Observações sobre bloqueios:
- Os bloqueios são considerados na granularidade de linha e coluna. Se a transação T1 bloqueou a coluna "A" da linha "foo" e a transação T2 quer gravar na coluna "B" da linha "foo", não há conflito.
- Gravações em um item de dados que também não leem os dados que estão sendo gravados (também conhecidas como "gravações cegas") não entram em conflito com outras gravações cegas do mesmo item. O carimbo de data/hora de confirmação de cada gravação determina a ordem em que ele é aplicado ao banco de dados. Uma consequência disso é que o Spanner só precisa fazer o upgrade para um bloqueio exclusivo se você leu os dados que está gravando. Caso contrário, o Spanner usa um bloqueio compartilhado de gravação.
- Ao realizar pesquisas de linhas em uma transação de leitura e gravação, use os índices secundários para limitar as linhas verificadas a um intervalo menor. Isso faz com que o Spanner bloqueie um número menor de linhas na tabela, permitindo modificação simultânea a linhas fora do intervalo.
Os bloqueios não precisam ser usados para garantir acesso exclusivo a um recurso fora do Spanner. As transações podem ser canceladas por vários motivos pelo Spanner, como ao permitir que os dados se movam pelos recursos de computação da instância. Se uma transação for repetida, seja explicitamente pelo código do aplicativo ou implicitamente pelo código do cliente, como o driver JDBC do Spanner, só é garantido que os bloqueios foram mantidos durante a tentativa que realmente foi executada.
É possível usar a ferramenta de introdução Estatísticas de bloqueio para investigar conflitos de bloqueio no seu banco de dados.
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. Por exemplo, pense no seguinte cenário: a transação Txn1
mantém um bloqueio no registro A
e está aguardando um bloqueio no registro B
, e Txn2
mantém um bloqueio no registro B
e está aguardando um bloqueio no registro A
. A única maneira de progredir nessa situação é cancelar uma das transações para liberar o bloqueio, permitindo que a outra transação continue.
O Spanner usa o algoritmo padrão "wound-wait" para lidar com a detecção de impasse. Por trás dos bastidores, o Spanner acompanha a idade de cada transação que solicita bloqueios conflitantes. Ele também permite que transações mais antigas anulem transações mais recentes. “Mais antigo” significa que a primeira leitura, consulta ou confirmação da transação aconteceu antes.
Ao dar prioridade a transações mais antigas, o Spanner garante que todas as transações tenham a chance de adquirir bloqueios com o tempo, depois que tiverem idade suficiente para ter prioridade mais alta que outras transações. Por exemplo, uma transação que adquire um bloqueio compartilhado do leitor pode ser interrompida por uma transação mais antiga que precisa de um bloqueio compartilhado do gravador.
Execução distribuída
O Spanner pode executar transações em dados que abrangem vários servidores. Esse avanço acaba reduzindo o desempenho em comparação com transações de servidor único.
Quais tipos de transações podem ser distribuídos? Em segundo plano, o Spanner pode dividir a responsabilidade por linhas no banco de dados entre vários servidores. Uma linha e as linhas correspondentes em tabelas intercaladas geralmente são disponibilizadas pelo mesmo servidor, assim como duas linhas na mesma tabela com as chaves próximas. O Spanner pode executar transações em linhas em diferentes servidores. No entanto, como uma regra geral, as transações que afetam muitas linhas colocalizadas são mais rápidas e mais baratas do que as transações que afetam muitas linhas espalhadas por todo o banco de dados ou em toda 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.
Propriedades
Uma transação somente leitura do Spanner executa um conjunto de leituras em um único ponto lógico no tempo, tanto da perspectiva da transação somente leitura quanto do ponto de vista de outros leitores e gravadores no banco de dados do Spanner. Isso significa que as transações somente leitura sempre estão de acordo com um estado consistente do banco de dados em um ponto escolhido no histórico das transações.
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
A seguir, mostramos como usar uma transação somente leitura para receber dados consistentes para duas leituras no mesmo carimbo de data/hora:
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Transações de DML particionada
Com a linguagem de manipulação de dados particionada (DML particionada, na sigla em inglês), é possível executar instruções UPDATE
e DELETE
em larga escala sem ultrapassar os limites de transação nem bloquear uma tabela inteira.
O Spanner particiona o espaço de chaves e executa as instruções DML em cada
partição em uma transação de leitura e gravação separada.
Execute as instruções DML em transações de leitura e gravação que você mesmo cria explicitamente no código. Para mais informações, consulte Como usar a DML.
Propriedades
É possível executar apenas uma instrução DML particionada por vez, esteja você usando um método de biblioteca de cliente ou a CLI do Google Cloud.
As transações particionadas não são compatíveis com confirmação ou reversão. O Spanner executa e aplica a instrução DML imediatamente. Se a operação for cancelada ou falhar, o Spanner cancelará todas as partições em execução e não iniciará as partições restantes. O Spanner não reverte partições que já foram executadas.
Interface
O Spanner oferece uma interface para executar uma única instrução DML particionada.
Examples
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.
C#
Use o método ExecutePartitionedUpdateAsync()
para executar uma instrução DML particionada.
Go
Use o método PartitionedUpdate()
para executar uma instrução DML particionada.
Java
Use o método executePartitionedUpdate()
para executar uma instrução DML particionada.
Node.js
Use o método runPartitionedUpdate()
para executar uma instrução DML particionada.
PHP
Use o método executePartitionedUpdate()
para executar uma instrução DML particionada.
Python
Use o método execute_partitioned_dml()
para executar uma instrução DML particionada.
Ruby
Use o método execute_partitioned_update()
para executar uma instrução DML particionada.
O exemplo de código a seguir exclui linhas da tabela Singers
com base na coluna SingerId
.