Esta página explica as transações no Spanner e inclui exemplos de código para executar transações.
Introdução
Uma transação no Spanner é um conjunto de leituras e gravações que executam atomicamente em um único ponto lógico no tempo em colunas, linhas e tabelas em 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/gravação pode cancelar, 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. Somente leitura transações não precisam ser confirmadas e não têm bloqueios. Além disso, as transações somente leitura podem aguardar a conclusão das gravações em andamento antes em execução.
DML particionada. Esse tipo de transação executa uma manipulação de dados de linguagem natural (DML) como DML particionada. A DML particionada foi projetada para atualizações e exclusões em massa, especialmente limpeza e preenchimento periódicos. Se você precisa comprometer um grande número gravações cegas, mas não exigem uma transação atômica, é possível fazer modificações em massa nas tabelas do Spanner usando 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 de transações em Spanner e introduz as funções de leitura e gravação, somente leitura e DML particionada e interfaces de transação 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? As transações de leitura/gravação fornecem o ID de bancos de dados relacionais. Na verdade, as funções de leitura e as transações oferecem garantias ainda mais fortes do que o ACID tradicional; consulte Semântica abaixo.
Isolamento
A seguir estão as propriedades de isolamento para leitura/gravação e somente leitura transações.
Transações que leem e gravam
Estas são as propriedades de isolamento que você recebe após confirmar um transação que contém uma série de leituras (ou consultas) e grava:
- Todas as leituras dentro da transação retornaram valores que refletem um resultado snapshot capturado no carimbo de data/hora de confirmação da transação.
- Linhas ou intervalos vazios permaneceram no momento da confirmação.
- Todas as gravações dentro da transação foram confirmadas no carimbo de data/hora de confirmação.
- As gravações só ficam visíveis para as transações depois da transação comprometido.
Alguns drivers de cliente do Spanner contêm lógica de repetição de transações para mascarar erros temporários, o que ocorre ao executar novamente a transação e validar o e 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 oferecem uma interface para executar um corpo de trabalho no contexto de uma transação de leitura/gravação, com novas tentativas para a transação aborta. Confira o contexto para explicar isso: um comando transação pode ter que ser testada várias vezes antes de ser confirmada. 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 um 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", ou seja, todas as transações aparecem como se tivessem sido executados em uma ordem serial, mesmo que algumas das leituras, gravações e outras operações de transações distintas realmente ocorreram 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 mais forte do que a capacidade de serialização, chamada Consistência externa: as transações são confirmadas em um pedido refletido nos carimbos de data/hora de commit, e esses carimbos Os carimbos de data/hora são mostrados em tempo real para que você possa compará-los ao 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ê tenha tentado cancelar a leitura, o Spanner não garantir que a leitura seja realmente cancelada. 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 os mesmos no seu 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ê realiza uma leitura como parte transação, o Spanner adquire bloqueios de leitura compartilhados, o que permite para continuar acessando os dados até que a transação esteja pronta para ser confirmada. 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 a modificação simultânea em linhas fora da do intervalo 10.240.0.0/16.
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: no Spanner, como, por exemplo, ao permitir que os dados se movimentem pelo aos recursos de computação dessa instância. Se houver uma nova tentativa de transação, seja explicitamente pelo código do aplicativo ou implicitamente por código de cliente, como driver JDBC do Spanner, ele é apenas os bloqueios foram mantidos durante a tentativa que foi realmente confirmada.
É 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 em impasse e
força o cancelamento de todas, 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 padrão "wound-wait" algoritmo para lidar com o impasse detecção de ameaças. Nos bastidores, o Spanner monitora 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 todos transação tenha a chance de adquirir bloqueios em algum momento, depois de se tornarem antigas o suficiente para ter maior prioridade 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 realizar transações em linhas de diferentes servidores; No entanto, como regra de as transações que afetam muitas linhas colocalizadas são mais rápidas e mais baratas do que transações que afetam muitas linhas espalhadas pelo banco de dados, ou em uma tabela grande.
As transações mais eficientes no Spanner incluem apenas as leituras 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, ambos da perspectiva da transação somente leitura em si e da perspectiva de outros leitores e escritores 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
Você só pode executar uma instrução DML particionada por vez, 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ção ou reversão. O Spanner executa e aplica a instrução DML imediatamente. Se você cancelar a operação, ou se a operação falhar, o Spanner vai cancelar todos os partições e não inicia nenhuma das partições restantes. O Spanner não reverte partições que já foram executadas.
Interface
O Spanner oferece uma interface para executar uma única DML particionada instrução.
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.
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
.