Visão geral dos níveis de isolamento

Nesta página, apresentamos diferentes níveis de isolamento e explicamos como eles funcionam no Spanner.

O nível de isolamento é uma propriedade do banco de dados que define quais dados ficam visíveis para transações simultâneas. O Spanner é compatível com dois dos níveis de isolamento definidos no padrão ANSI/ISO SQL: serializável e leitura repetida. Ao criar uma transação, é necessário escolher o nível de isolamento mais adequado. O nível de isolamento escolhido permite que transações individuais priorizem vários fatores, como latência, taxa de interrupção e se o aplicativo está sujeito aos efeitos de anomalias de dados. A melhor escolha depende das demandas específicas da carga de trabalho.

Isolamento serializável

O isolamento serializável é o nível de isolamento padrão no Spanner. Com o isolamento serializável, o Spanner oferece as garantias de controle de simultaneidade mais rigorosas para transações, o que é chamado de consistência externa. O Spanner se comporta como se todas as transações fossem executadas sequencialmente, mesmo que na verdade ele as execute em vários servidores (e possivelmente em vários datacenters) para maior desempenho e disponibilidade do que bancos de dados de servidor único. Além disso, se uma transação é concluída antes de outra começar a confirmação, o Spanner garante que os clientes sempre vejam os resultados das transações em ordem sequencial. De maneira intuitiva, o Spanner é semelhante a um banco de dados de uma única máquina.

A compensação é que o Spanner pode cancelar transações se uma carga de trabalho tiver alta disputa de leitura-gravação, em que muitas transações leem dados que outra transação está atualizando, devido à natureza fundamental das transações serializáveis. No entanto, esse é um bom padrão para um banco de dados operacional. Isso ajuda a evitar problemas de tempo complicados que geralmente surgem apenas com alta simultaneidade. Esses problemas são difíceis de reproduzir e solucionar. Portanto, o isolamento serializável oferece a melhor proteção contra anomalias de dados. Se uma transação precisar ser repetida, poderá haver um aumento na latência devido a essas repetições.

Isolamento de leitura repetível

No Spanner, o isolamento de leitura repetível é implementado usando uma técnica conhecida como isolamento de snapshot. O isolamento de leitura repetível no Spanner garante que todas as operações de leitura em uma transação vejam um snapshot consistente ou forte do banco de dados no momento em que a transação foi iniciada. Também garante que gravações simultâneas nos mesmos dados só serão bem-sucedidas se não houver conflitos. Essa abordagem é útil em cenários de alto conflito de leitura/gravação em que várias transações leem dados que outras transações podem estar modificando. Ao usar um snapshot fixo, a leitura repetível evita os impactos de desempenho do nível de isolamento serializável mais restritivo. As leituras podem ser executadas sem adquirir bloqueios e sem bloquear gravações simultâneas, o que resulta em menos transações canceladas que podem precisar ser repetidas devido a possíveis conflitos de serialização. Em casos de uso em que os clientes já executam tudo em uma transação de leitura/gravação e é difícil redesenhar e usar transações somente leitura, é possível 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 repetida pode levar a anomalias de dados se o aplicativo depender de relações ou restrições de dados específicas que não são aplicadas pelo esquema do banco de dados, especialmente quando a ordem das operações é importante. Nesses casos, uma transação pode ler dados, tomar decisões com base neles e gravar mudanças que violam essas restrições específicas do aplicativo, mesmo que as restrições de esquema do banco de dados ainda sejam atendidas. Isso acontece porque o isolamento de leitura repetida permite que transações simultâneas sejam processadas sem serialização estrita. Uma possível anomalia é conhecida como distorção de gravação, que surge de um tipo específico de atualização simultânea, em que cada atualização é aceita de forma independente, mas o efeito combinado viola a integridade dos dados do aplicativo. Por exemplo, imagine um sistema hospitalar em que pelo menos um médico precisa estar de plantão o tempo todo, e os médicos podem pedir para sair do plantão em um turno. No isolamento de leitura repetível, se o Dr. Richards e a Dra. Smith estiverem programados para ficar de plantão no mesmo turno e tentarem solicitar a remoção do plantão ao mesmo tempo, cada solicitação será bem-sucedida em paralelo. Isso acontece porque as duas transações leem que há pelo menos outro médico agendado para estar de plantão no início da transação, causando uma anomalia de dados se as transações forem bem-sucedidas. Por outro lado, o uso do isolamento serializável impede que essas transações violem a restrição, porque as transações serializáveis detectam possíveis anomalias de dados e interrompem a transação. Assim, garantindo a consistência do aplicativo ao aceitar taxas de interrupção mais altas.

No exemplo anterior, é possível usar a cláusula SELECT FOR UPDATE no isolamento de leitura repetível. A cláusula SELECT…FOR UPDATE verifica se os dados lidos no snapshot escolhido permanecem inalterados no momento da confirmação. Da mesma forma, instruções DML e mutações, que leem dados internamente para garantir a integridade das gravações, também verificam se os dados permanecem inalterados no momento da confirmação.

Para mais informações, consulte Usar isolamento de leitura repetível.

Exemplo de caso de uso:

O exemplo a seguir demonstra o benefício de usar o isolamento de leitura repetível para eliminar a sobrecarga de bloqueio. Tanto Transaction 1 quanto Transaction 2 são executados em isolamento de leitura repetível.

Transaction 1 estabelece um carimbo de data/hora de snapshot quando a instruçã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 um carimbo de data/hora de snapshot depois que Transaction 1 começa, mas antes de ser confirmado. Como o 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 depois que Transaction 2 é confirmado.

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 retornado pelo Spanner é a soma do orçamento lido por Transaction 1. Essa soma reflete apenas os dados presentes no snapshot T1. Ele não inclui o orçamento adicionado por Transaction 2, porque Transaction 2 fez o commit depois que Transaction 1 estabeleceu o snapshot T1. Usar leitura repetível significa que Transaction 1 não precisou ser interrompido, mesmo que Transaction 2 tenha modificado os dados lidos por Transaction 1. No entanto, o resultado retornado pelo Spanner pode ou não ser o resultado pretendido.

Conflitos de leitura e gravação e correção

No exemplo anterior, se os dados consultados pelas instruções SELECT em Transaction 1 fossem usados para tomar decisões subsequentes de orçamento de marketing, poderia haver problemas de correção.

Por exemplo, suponha que haja um orçamento total de 400,000. Com base no resultado da instrução SELECT em Transaction 1, podemos pensar que há 100,000 restante no orçamento e decidir alocar tudo para 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 é confirmado com sucesso, mesmo que Transaction 2 já tenha alocado 50,000 do orçamento restante de 100,000 para um novo álbum AlbumId = 5.

É possível usar a sintaxe SELECT...FOR UPDATE para validar se determinadas leituras de uma transação permanecem inalteradas durante o ciclo de vida dela e garantir a correção. No exemplo a seguir usando SELECT...FOR UPDATE, Transaction 1 é interrompido no momento do commit.

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 Usar SELECT FOR UPDATE no isolamento de leitura repetível.

Conflitos de gravação e correção

Ao usar o nível de isolamento de leitura repetível, as gravações simultâneas nos mesmos dados só serão bem-sucedidas se não houver conflitos.

No exemplo a seguir, Transaction 1 estabelece um carimbo de data/hora de snapshot na primeira instruçã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 Transaction 2 a seguir lê os mesmos dados que Transaction 1 e insere um novo item. Transaction 2 é confirmado sem esperar ou ser cancelado.

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 depois que Transaction 2 é confirmado.

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 é interrompido porque Transaction 2 já confirmou uma inserção na linha AlbumId = 5.

A seguir