Esta página apresenta diferentes níveis de isolamento e explica 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 oferece suporte a dois dos níveis de isolamento definidos no padrão ANSI/ISO SQL: serializável e leitura repetível. Ao criar uma transação, é necessário escolher o nível de isolamento mais adequado para ela. 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 é suscetível 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, que é chamada de consistência externa. O Spanner se comporta como se todas as transações fossem executadas sequencialmente, mesmo que na verdade o Spanner 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 for concluída antes que outra comece a ser confirmada, o Spanner garante que os clientes sempre vejam os resultados das transações em ordem sequencial. Intuitivamente, o Spanner é semelhante a um banco de dados de máquina única.
A desvantagem é que o Spanner pode interromper transações se uma carga de trabalho tiver alta contenção 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, essa é uma boa opção padrão para um banco de dados operacional. Ele 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 proteção mais forte contra anomalias de dados. Se uma transação precisar ser repetida, poderá 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 usando uma técnica comumente 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, como ele existia no início da transação. Ele também garante que gravações simultâneas nos mesmos dados só sejam bem-sucedidas se não houver conflitos. Essa abordagem é benéfica 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.
Com a simultaneidade otimista padrão
,
as leituras são executadas sem adquirir bloqueios e sem bloquear gravações simultâneas, o que resulta em menos transações interrompidas que podem precisar ser
repetidas devido a possíveis conflitos de serialização. Com
simultaneidade pessimista,
as operações de leitura usam snapshots, mas bloqueios exclusivos se aplicam a dados lidos de
FOR UPDATE consultas ou lock_scanned_ranges=exclusive dicas, e dados gravados
com consultas DML.
Para cargas de trabalho que migram de outros bancos de dados, recomendamos configurar o aplicativo para usar o isolamento de leitura repetível no Spanner. A semântica de transação de leitura repetível, especificamente o bloqueio para leituras, corresponde aos níveis de isolamento padrão na maioria dos outros bancos de dados (por exemplo, MySQL e PostgreSQL). Isso ajuda a reduzir a necessidade de reformular o aplicativo para trabalhar com o nível de isolamento serializável padrão do Spanner.
Ao contrário do isolamento serializável, a leitura repetível 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 nesses dados e gravar alterações que violam essas restrições específicas do aplicativo, mesmo que as restrições do esquema do banco de dados ainda sejam atendidas. Isso acontece porque o isolamento de leitura repetível permite que transações simultâneas prossigam 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 que há um sistema hospitalar em que pelo menos um médico precisa estar de plantão o tempo todo, e os médicos podem solicitar a dispensa do plantão em um turno. No isolamento de leitura repetível, se o Dr. Richards e a Dra. Smith estiverem programados para o mesmo turno e tentarem solicitar a dispensa do plantão simultaneamente, cada solicitação será bem-sucedida em paralelo. Isso ocorre porque ambas as transações leem que há pelo menos outro médico programado para o plantão no início da transação, causando 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, garante a consistência do aplicativo aceitando 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,
as 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. Além disso, com a simultaneidade pessimista, os dados lidos pelo SELECT ... FOR UPDATE e os dados gravados por instruções DML adquirem bloqueios exclusivos para impedir que transações futuras confirmem modificações conflitantes antes que a transação atual seja confirmada.
Para cargas de trabalho que migram de outros bancos de dados que usam consultas FOR UPDATE, recomendamos configurar o aplicativo para usar o isolamento de leitura repetível com simultaneidade pessimista no Spanner. O aplicativo continua a adquirir bloqueios para os dados lidos pelo SELECT ... FOR UPDATE, que é o comportamento padrão em outros bancos de dados.
Para mais informações, consulte Usar o 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. As Transaction 1 e Transaction 2 são executadas no 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 após o início de Transaction 1, mas antes da confirmação. Como 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 a confirmação 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 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 foi confirmado depois que Transaction 1 estabeleceu o snapshot T1. O uso da leitura repetível significa que Transaction 1 não precisou ser interrompida, 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-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 restantes 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 é confirmada 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 da transação para garantir a correção dela. No exemplo a seguir usando SELECT...FOR UPDATE, Transaction 1 é interrompida 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 Usar SELECT FOR UPDATE no isolamento de leitura repetível.
Também é possível usar a simultaneidade pessimista, que adquire bloqueios exclusivos em dados lidos pela instrução SELECT...FOR UPDATE. Por exemplo, Transaction 1 é interrompida no momento da confirmação porque Transaction 2 confirmou as modificações antes que Transaction 1 adquirisse bloqueios, o que resulta em um conflito. No entanto, se a ordem da transação fizer com que Transaction 2 tente atualizar o orçamento de marketing depois que Transaction 1 adquirir bloqueios, Transaction 2 vai aguardar que Transaction 1 confirme e libere os bloqueios antes de prosseguir. A opção de simultaneidade pessimista serializa o acesso aos dados.
Para mais informações, consulte Controle de simultaneidade.
Conflitos de gravação-gravação e correção
Ao usar o nível de isolamento de leitura repetível, 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;
A Transaction 2 a seguir lê os mesmos dados que Transaction 1 e insere um novo item. Transaction 2 é confirmada com sucesso sem esperar ou interromper.
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 a confirmação 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 é interrompida porque Transaction 2 já confirmou uma inserção na linha AlbumId = 5.
A seguir
Saiba como usar o nível de isolamento de leitura repetível.
Saiba mais sobre o controle de simultaneidade.
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 TrueTime e consistência externa.