Database
 sql >> Base de Dados >  >> RDS >> Database

O Nível de Isolamento INSTANTÂNEO

[ Veja o índice para toda a série ]

Os problemas de simultaneidade são difíceis da mesma forma que a programação multi-thread é difícil. A menos que o isolamento serializável seja usado, pode ser difícil codificar transações T-SQL que sempre funcionarão corretamente quando outros usuários estiverem fazendo alterações no banco de dados ao mesmo tempo.

Os problemas potenciais podem não ser triviais mesmo se a 'transação' em questão for um simples SELECT demonstração. Para transações complexas de várias instruções que leem e gravam dados, o potencial para resultados inesperados e erros em alta simultaneidade pode se tornar rapidamente esmagador. A tentativa de resolver problemas de simultaneidade sutis e difíceis de reproduzir aplicando dicas de bloqueio aleatórias ou outros métodos de tentativa e erro pode ser uma experiência extremamente frustrante.

Em muitos aspectos, o nível de isolamento de instantâneo parece uma solução perfeita para esses problemas de simultaneidade. A ideia básica é que cada transação de instantâneo se comporte como se fosse executada em sua própria cópia privada do estado confirmado do banco de dados, obtido no momento em que a transação foi iniciada. Fornecer a toda a transação uma visão imutável dos dados confirmados obviamente garante resultados consistentes para operações somente leitura, mas e as transações que alteram os dados?

O isolamento de instantâneo lida com as alterações de dados de maneira otimista, assumindo implicitamente que os conflitos entre gravadores simultâneos serão relativamente raros. Onde ocorre um conflito de gravação, o primeiro committer vence e a transação perdedora tem suas alterações revertidas. É lamentável para a transação revertida, é claro, mas se isso for uma ocorrência rara o suficiente, os benefícios do isolamento de instantâneos podem facilmente superar os custos de uma falha ocasional e de uma nova tentativa.

A semântica relativamente simples e limpa do isolamento de instantâneos (quando comparada com as alternativas) pode ser uma vantagem significativa, principalmente para pessoas que não trabalham exclusivamente no mundo do banco de dados e, portanto, não conhecem bem os vários níveis de isolamento. Mesmo para profissionais de banco de dados experientes, um nível de isolamento relativamente "intuitivo" pode ser um alívio bem-vindo.

É claro que as coisas raramente são tão simples quanto parecem à primeira vista, e o isolamento de instantâneos não é exceção. A documentação oficial faz um bom trabalho ao descrever as principais vantagens e desvantagens do isolamento de instantâneos, portanto, a maior parte deste artigo se concentra em explorar alguns dos problemas menos conhecidos e surpreendentes que você pode encontrar. Primeiro, porém, uma rápida olhada nas propriedades lógicas deste nível de isolamento:

Propriedades de ACID e isolamento de instantâneo


O isolamento de instantâneo não é um dos níveis de isolamento definidos no SQL Standard, mas ainda é frequentemente comparado usando os 'fenômenos de simultaneidade' definidos lá. Por exemplo, a seguinte tabela de comparação é reproduzida do artigo técnico do SQL Server, "SQL Server 2005 Row Versioning-Based Transaction Isolation" de Kimberly L. Tripp e Neal Graves:



Ao fornecer uma visualização pontual de dados confirmados , o isolamento de instantâneo fornece proteção contra todos os três fenômenos de simultaneidade mostrados lá. As leituras sujas são evitadas porque apenas os dados confirmados são visíveis e a natureza estática do instantâneo impede que leituras não repetíveis e fantasmas sejam encontradas.

No entanto, essa comparação (e a seção destacada em particular) mostra apenas que o instantâneo e os níveis de isolamento serializável evitam os mesmos três fenômenos específicos. Isso não significa que sejam equivalentes em todos os aspectos. É importante ressaltar que o padrão SQL-92 não define o isolamento serializável apenas em termos dos três fenômenos. A seção 4.28 da norma fornece a definição completa:

A execução de transações SQL simultâneas no nível de isolamento SERIALIZABLE é garantida como serializável. Uma execução serializável é definida como uma execução das operações de transações SQL executadas simultaneamente que produz o mesmo efeito que alguma execução serial dessas mesmas transações SQL. Uma execução serial é aquela em que cada transação SQL é executada até a conclusão antes do início da próxima transação SQL.

A extensão e a importância das garantias implícitas aqui são muitas vezes perdidas. Para declará-lo em linguagem simples:

Qualquer transação serializável que seja executada corretamente quando executada sozinha continuará a ser executada corretamente com qualquer combinação de transações simultâneas ou será revertida com uma mensagem de erro (normalmente um deadlock na implementação do SQL Server).

Níveis de isolamento não serializáveis, incluindo isolamento de instantâneo, não fornecem as mesmas fortes garantias de correção.

Dados obsoletos


O isolamento de instantâneos parece quase sedutoramente simples. As leituras sempre vêm de dados confirmados a partir de um único ponto no tempo, e os conflitos de gravação são detectados e tratados automaticamente. Como isso não é uma solução perfeita para todas as dificuldades relacionadas à simultaneidade?

Um possível problema é que as leituras de instantâneo não refletem necessariamente o estado atual de confirmação do banco de dados. Uma transação de instantâneo ignora completamente quaisquer alterações confirmadas feitas por outras transações simultâneas após o início da transação de instantâneo. Outra maneira de colocar isso é dizer que uma transação de instantâneo vê dados obsoletos e desatualizados. Embora esse comportamento possa ser exatamente o necessário para gerar um relatório pontual preciso, ele pode não ser tão adequado em outras circunstâncias (por exemplo, quando usado para impor uma regra em um acionador).

Escrever enviesamento


O isolamento de instantâneo também é vulnerável a um fenômeno relacionado conhecido como distorção de gravação. A leitura de dados obsoletos desempenha um papel nisso, mas esse problema também ajuda a esclarecer o que a 'detecção de conflito de gravação' faz ou não.

A distorção de gravação ocorre quando duas transações simultâneas leem dados que a outra transação modifica. Nenhum conflito de gravação ocorre porque as duas transações modificam linhas diferentes. Nenhuma das transações vê as alterações feitas pela outra, porque ambas estão lendo de um ponto no tempo anterior a essas alterações.

Um exemplo clássico de distorção de escrita é o problema do mármore branco e preto, mas quero mostrar outro exemplo simples aqui:
-- Create two empty tables
CREATE TABLE A (x integer NOT NULL);
CREATE TABLE B (x integer NOT NULL);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT A (x) SELECT COUNT_BIG(*) FROM B;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT B (x) SELECT COUNT_BIG(*) FROM A;
COMMIT TRANSACTION;
 
-- Connection 1
COMMIT TRANSACTION;

Sob o isolamento de instantâneo, ambas as tabelas nesse script terminam com uma única linha contendo um valor zero. Este é um resultado correto, mas não é serializável:não corresponde a nenhuma ordem de execução de transação serial possível. Em qualquer agendamento verdadeiramente serial, uma transação deve ser concluída antes que a outra comece, de modo que a segunda transação contaria a linha inserida pela primeira. Isso pode soar como um detalhe técnico, mas lembre-se de que as poderosas garantias serializáveis ​​só se aplicam quando as transações são realmente serializáveis.

Uma sutileza na detecção de conflitos


Um conflito de gravação de instantâneo ocorre sempre que uma transação de instantâneo tenta modificar uma linha que foi modificada por outra transação confirmada após o início da transação de instantâneo. Há duas sutilezas aqui:
  1. As transações não precisam alterar quaisquer valores de dados; e
  2. As transações não precisam modificar nenhuma coluna comum .

O script a seguir demonstra ambos os pontos:
-- Test table
CREATE TABLE dbo.Conflict
(
    ID1 integer UNIQUE,
    Value1 integer NOT NULL,
    ID2 integer UNIQUE,
    Value2 integer NOT NULL
);
 
-- Insert one row
INSERT dbo.Conflict
    (ID1, ID2, Value1, Value2)
VALUES
    (1, 1, 1, 1);
 
-- Connection 1
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value1 = 1
WHERE ID1 = 1;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value2 = 1
WHERE ID2 = 1;
 
-- Connection 1
COMMIT TRANSACTION;

Observe o seguinte:
  • Cada transação localiza a mesma linha usando um índice diferente
  • Nenhuma atualização resulta em uma alteração nos dados já armazenados
  • As duas transações 'atualizam' colunas diferentes na linha.

Apesar de tudo isso, quando a primeira transação é confirmada, a segunda transação termina com um erro de conflito de atualização:



Resumo:A detecção de conflitos sempre opera no nível de uma linha inteira e uma 'atualização' não precisa alterar nenhum dado. (Caso você esteja se perguntando, as alterações nos dados LOB ou SLOB fora da linha também contam como uma alteração na linha para fins de detecção de conflitos).

O problema da chave estrangeira


A detecção de conflito também se aplica à linha pai em um relacionamento de chave estrangeira. Ao modificar uma linha filho no isolamento de instantâneo, uma alteração na linha pai em outra transação pode desencadear um conflito. Como antes, essa lógica se aplica a toda a linha pai – a atualização pai não precisa afetar a própria coluna de chave estrangeira. Qualquer operação na tabela filha que exija uma verificação automática de chave estrangeira no plano de execução pode resultar em um conflito inesperado.

Para demonstrar isso, primeiro crie as seguintes tabelas e dados de exemplo:
CREATE TABLE dbo.Dummy
(
    x integer NULL
);
 
CREATE TABLE dbo.Parent
(
    ParentID integer PRIMARY KEY,
    ParentValue integer NOT NULL
);
 
CREATE TABLE dbo.Child 
(
    ChildID integer PRIMARY KEY,
    ChildValue integer NOT NULL,
    ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent
);
 
INSERT dbo.Parent 
    (ParentID, ParentValue) 
VALUES (1, 1);
 
INSERT dbo.Child 
    (ChildID, ChildValue, ParentID) 
VALUES (1, 1, 1);

Agora execute o seguinte a partir de duas conexões separadas, conforme indicado nos comentários:
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.Dummy;
 
-- Connection 2 (any isolation level)
UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1;
 
-- Connection 1
UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1;
UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;

A leitura da tabela fictícia existe para garantir que a transação do instantâneo tenha sido iniciada oficialmente. Emitindo BEGIN TRANSACTION não é suficiente para fazer isso; temos que realizar algum tipo de acesso a dados em uma tabela de usuário.

A primeira atualização da tabela Filho não causa conflito porque definir a coluna de referência como NULL não requer uma verificação da tabela pai no plano de execução (não há nada para verificar). O processador de consulta não toca na linha pai no plano de execução, portanto, não há conflito.

A segunda atualização da tabela Child aciona um conflito porque uma verificação de chave estrangeira é executada automaticamente. Quando a linha Pai é acessada pelo processador de consulta, ela também é verificada quanto a um conflito de atualização. Um erro é gerado nesse caso porque a linha Pai referenciada passou por uma modificação confirmada após o início da transação de instantâneo. Observe que a modificação da tabela pai não afetou a própria coluna de chave estrangeira.

Um conflito inesperado também pode ocorrer se uma alteração na tabela Filho referenciar uma linha Pai que foi criada por uma transação simultânea (e essa transação confirmada após o início da transação de instantâneo).

Resumo:Um plano de consulta que inclui uma verificação automática de chave estrangeira pode gerar um erro de conflito se a linha referenciada tiver sofrido algum tipo de modificação (incluindo criação!) desde o início da transação do instantâneo.

O problema da tabela truncada


Uma transação de instantâneo falhará com um erro se qualquer tabela acessada tiver sido truncada desde o início da transação. Isso se aplica mesmo se a tabela truncada não tiver linhas para começar, como o script abaixo demonstra:
CREATE TABLE dbo.AccessMe
(
    x integer NULL
);
 
CREATE TABLE dbo.TruncateMe
(
    x integer NULL
);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.AccessMe;
 
-- Connection 2
TRUNCATE TABLE dbo.TruncateMe;
 
-- Connection 1
SELECT COUNT_BIG(*) FROM dbo.TruncateMe;

O SELECT final falha com um erro:



Esse é outro efeito colateral sutil a ser verificado antes de habilitar o isolamento de instantâneo em um banco de dados existente.

Próxima vez


O próximo (e último) post desta série falará sobre o nível de isolamento não confirmado de leitura (carinhosamente conhecido como "nolock").
[ Veja o índice para toda a série ]