Vista geral dos níveis de isolamento

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 1T1. 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?