Esta página apresenta diferentes níveis de isolamento e explica como funcionam no Spanner.
O nível de isolamento é uma propriedade da base de dados que define que dados são visíveis para transações simultâneas. O Spanner suporta dois dos níveis de isolamento definidos na norma SQL ANSI/ISO: serializável e leitura repetível. Quando cria uma transação, tem de escolher o nível de isolamento mais adequado para a transação. O nível de isolamento escolhido permite que as transações individuais deem prioridade a vários fatores, como a latência, a taxa de anulação e se a aplicação é suscetível aos efeitos das anomalias de dados. A melhor escolha depende das exigências específicas da carga de trabalho.
Isolamento serializável
O isolamento serializável é o nível de isolamento predefinido no Spanner. No isolamento serializável, o Spanner oferece-lhe as garantias de controlo de concorrência mais rigorosas para transações, o que se denomina consistência externa. O Spanner comporta-se como se todas as transações fossem executadas sequencialmente, embora o Spanner as execute realmente em vários servidores (e, possivelmente, em vários centros de dados) para um desempenho e uma disponibilidade superiores aos das bases de dados de servidor único. Além disso, se uma transação for concluída antes de outra começar a ser confirmada, o Spanner garante que os clientes veem sempre os resultados das transações por ordem sequencial. Intuitivamente, o Spanner é semelhante a uma base de dados de uma única máquina.
A desvantagem é que o Spanner pode anular transações se uma carga de trabalho tiver uma elevada contenção de leitura/escrita, em que muitas transações leem dados que outra transação está a atualizar, devido à natureza fundamental das transações serializáveis. No entanto, esta é uma boa predefinição para uma base de dados operacional. Ajuda a evitar problemas de sincronização complexos que normalmente só surgem com uma concorrência elevada. Estes problemas são difíceis de reproduzir e resolver. Por conseguinte, o isolamento serializável oferece a proteção mais forte contra anomalias de dados. Se for necessário tentar novamente uma transação, pode haver um aumento na latência devido a novas tentativas de transação.
Isolamento de leitura repetível
No Spanner, o isolamento de leitura repetível é implementado através de uma técnica conhecida como isolamento de instantâneos. O isolamento de leitura repetível no Spanner garante que todas as operações de leitura numa transação veem uma imagem consistente ou forte da base de dados tal como existia no início da transação. Também garante que as escritas simultâneas nos mesmos dados só têm êxito se não existirem conflitos. Esta abordagem é vantajosa em cenários de conflitos de leitura/escrita elevados, em que várias transações leem dados que outras transações podem estar a modificar. A utilização de uma captura instantânea fixa, a leitura repetível, evita os impactos no desempenho do nível de isolamento serializável mais restritivo. As leituras podem ser executadas sem adquirir bloqueios e sem bloquear escritas simultâneas, o que resulta em menos transações anuladas que podem ter de ser repetidas devido a potenciais conflitos de serialização. Nos exemplos de utilização em que os seus clientes já executam tudo numa transação de leitura/escrita e é difícil redesenhar e usar transações só de leitura, pode usar o isolamento de leitura repetível para melhorar a latência das suas cargas de trabalho.
Ao contrário do isolamento serializável, a leitura repetível pode originar anomalias de dados se a sua aplicação depender de relações ou restrições de dados específicas que não sejam aplicadas pelo esquema da base de dados, especialmente quando a ordem das operações é importante. Nesses casos, uma transação pode ler dados, tomar decisões com base nesses dados e, em seguida, escrever alterações que violem essas restrições específicas da aplicação, mesmo que as restrições do esquema da base de dados ainda sejam cumpridas. Isto acontece porque o isolamento de leitura repetível permite que as transações simultâneas prossigam sem serialização rigorosa. Uma potencial anomalia é conhecida como uma distorção de escrita, que surge de um tipo específico de atualização concorrente, em que cada atualização é aceite de forma independente, mas o respetivo efeito combinado viola a integridade dos dados da aplicação. Por exemplo, imagine que existe um sistema hospitalar em que, pelo menos, um médico tem de estar de prevenção em todos os momentos e os médicos podem pedir para não estar de prevenção durante um turno. No isolamento de leitura repetível, se o Dr. Richards e a Dra. Smith estiverem agendados para estar de prevenção no mesmo turno e tentarem simultaneamente pedir para deixar de estar de prevenção, cada pedido é bem-sucedido em paralelo. Isto deve-se ao facto de ambas as transações lerem que existe, pelo menos, outro médico agendado para estar de serviço no início da transação, o que causa uma anomalia nos dados se as transações forem bem-sucedidas. Por outro lado, a utilização do isolamento serializável impede que estas transações violem a restrição, uma vez que as transações serializáveis detetam potenciais anomalias de dados e interrompem a transação. Deste modo, garante a consistência da aplicação aceitando taxas de anulação mais elevadas.
No exemplo anterior, pode
usar a cláusula SELECT FOR UPDATE
no isolamento de leitura repetível.
A cláusula SELECT…FOR UPDATE
verifica se os dados que leu na cópia instantânea escolhida permanecem inalterados no momento da confirmação. Da mesma forma, as declarações DML e as mutações, que leem dados internamente para garantir a integridade das escritas, também verificam se os dados permanecem inalterados no momento da confirmação.
Para mais informações, consulte o artigo Use o isolamento de leitura repetível.
Exemplo de utilização
O exemplo seguinte demonstra a vantagem de usar o isolamento de leitura repetível para eliminar a sobrecarga de bloqueio. Tanto o Transaction 1
como o Transaction 2
são executados no isolamento de leitura repetível.
Transaction 1
estabelece uma data/hora do instantâneo quando a declaração SELECT
é executada.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
/*-----------+------------------*
| AlbumId | MarketingBudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
/*-----------+------------------*
| albumid | marketingbudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
Em seguida, Transaction 2
estabelece uma data/hora de instantâneo após o início da transação, mas antes de a confirmar.Transaction 1
Uma vez que Transaction 1
não atualizou os dados, a consulta SELECT
em Transaction 2
lê os mesmos dados que Transaction 1
.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
Transaction 1
continua após o compromisso de Transaction 2
.
GoogleSQL
-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;
/*-----------*
| UsedBudget |
+------------+
| 300000 |
*------------*/
PostgreSQL
-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;
/*-----------*
| usedbudget |
+------------+
| 300000 |
*------------*/
O valor UsedBudget
que o Spanner devolve é a soma do orçamento lido por Transaction 1
. Esta soma reflete apenas os dados presentes na
T1
imagem instantânea. Não inclui o orçamento que Transaction 2
adicionou, porque Transaction 2
comprometeu-se após a criação da imagem instantânea estabelecida Transaction 1
T1
. A utilização da leitura repetível significa que Transaction 1
não teve de abortar, mesmo que Transaction 2
tenha modificado os dados lidos por Transaction 1
. No entanto, o resultado devolvido pelo Spanner pode ou não ser o resultado pretendido.
Conflitos de leitura/escrita e precisão
No exemplo anterior, se os dados consultados pelas declarações SELECT
em
Transaction 1
fossem usados para tomar decisões subsequentes sobre o orçamento de marketing, poderiam existir problemas de exatidão.
Por exemplo, suponhamos que existe um orçamento total de 400,000
. Com base no resultado da declaração SELECT
em Transaction 1
, podemos pensar que restam 100,000
no orçamento e decidir atribuir tudo a AlbumId = 4
.
GoogleSQL
-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;
COMMIT;
PostgreSQL
-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;
COMMIT;
Transaction 1
é confirmada com êxito, apesar de Transaction 2
já ter
atribuído 50,000
do orçamento restante a um novo álbum
AlbumId = 5
.100,000
Pode usar a sintaxe SELECT...FOR UPDATE
para validar se determinadas leituras de uma transação permanecem inalteradas durante o ciclo de vida da transação, de modo a garantir a correção da transação. No exemplo seguinte, usando SELECT...FOR UPDATE
, Transaction 1
é anulado no momento da confirmação.
GoogleSQL
-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;
/*-----------*
| TotalBudget |
+------------+
| 300000 |
*------------*/
COMMIT;
PostgreSQL
-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;
/*-------------*
| totalbudget |
+-------------+
| 300000 |
*-------------*/
COMMIT;
Para mais informações, consulte o artigo Use SELECT FOR UPDATE in repeatable read isolation (Use SELECT FOR UPDATE no isolamento de leitura repetível).
Conflitos de escrita e correção
Ao usar o nível de isolamento de leitura repetível, as escritas simultâneas nos mesmos dados só são bem-sucedidas se não houver conflitos.
No exemplo seguinte, Transaction 1
estabelece uma data/hora de instantâneo na primeira declaração SELECT
.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
O seguinte Transaction 2
lê os mesmos dados que Transaction 1
e insere um novo item. Transaction 2
é confirmado com êxito sem esperar nem anular.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
Transaction 1
continua após o compromisso de Transaction 2
.
GoogleSQL
-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
PostgreSQL
-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
Transaction 1
aborts since Transaction 2
already committed an insertion
to the AlbumId = 5
row.
O que se segue?
Saiba como usar o nível de isolamento de leitura repetível.
Saiba como usar SELECT FOR UPDATE no isolamento de leitura repetível.
Saiba como usar SELECT FOR UPDATE no isolamento serializável.
Para saber mais sobre a serialização e a consistência externa do Spanner, consulte o artigo TrueTime e consistência externa.