O SQL Server fornece duas implementações físicas da leitura confirmada nível de isolamento definido pelo padrão SQL, bloqueio de leitura confirmada e isolamento de instantâneo de leitura confirmada (RCSI ). Embora ambas as implementações atendam aos requisitos estabelecidos no padrão SQL para comportamentos de isolamento de leitura confirmada, o RCSI tem comportamentos físicos bastante diferentes da implementação de bloqueio que analisamos na postagem anterior desta série.
Garantias Lógicas
O padrão SQL requer que uma transação operando no nível de isolamento de leitura confirmada não sofra leituras sujas. Outra maneira de expressar esse requisito é dizer que uma transação confirmada de leitura deve encontrar apenas dados confirmados .
O padrão também diz que ler transações confirmadas pode experimentar os fenômenos de simultaneidade conhecidos como leituras não repetíveis e fantasmas (embora eles não sejam realmente obrigados a fazê-lo). Por acaso, ambas as implementações físicas de isolamento de leitura confirmada no SQL Server podem experimentar leituras não repetíveis e linhas fantasmas, embora os detalhes precisos sejam bem diferentes.
Uma visualização pontual dos dados confirmados
Se a opção de banco de dados
READ_COMMITTED_SNAPSHOT
em ON
, o SQL Server usa uma implementação de controle de versão de linha do nível de isolamento de leitura confirmada. Quando ativado, as transações que solicitam o isolamento de leitura confirmada usam automaticamente a implementação RCSI; nenhuma alteração no código T-SQL existente é necessária para usar o RCSI. Observe com atenção que isso não é o mesmo como dizer que o código se comportará da mesma forma sob RCSI como ao usar a implementação de bloqueio de leitura confirmada, na verdade, isso geralmente não é o caso . Não há nada no padrão SQL que exija que os dados lidos por uma transação com confirmação de leitura sejam os mais recentes dados comprometidos. A implementação do SQL Server RCSI aproveita isso para fornecer às transações uma visualização pontual de dados confirmados, em que esse ponto no tempo é o momento em que a instrução atual começou execução (não no momento em que qualquer transação contida foi iniciada).
Isso é bem diferente do comportamento da implementação de bloqueio do SQL Server de leitura confirmada, em que a instrução vê os dados confirmados mais recentemente a partir do momento em que cada item é lido fisicamente . Bloquear a leitura confirmada libera os bloqueios compartilhados o mais rápido possível, de modo que o conjunto de dados encontrado pode vir de pontos muito diferentes no tempo.
Para resumir, o bloqueio de leitura confirmada vê cada linha como estava na época, foi brevemente trancado e lido fisicamente; RCSI vê todas as linhas como estavam no momento em que a declaração começou. Ambas as implementações têm a garantia de nunca ver dados não confirmados, mas os dados encontrados podem ser muito diferentes.
As implicações de uma visualização pontual
Ver uma visão pontual dos dados confirmados pode parecer evidentemente superior ao comportamento mais complexo da implementação de bloqueio. Está claro, por exemplo, que uma visualização pontual não pode sofrer com os problemas de linhas ausentes ou encontrar a mesma linha várias vezes , que são ambos possíveis sob bloqueio de isolamento de leitura confirmada.
Uma segunda vantagem importante do RCSI é que ele não adquire bloqueios compartilhados ao ler dados, porque os dados vêm do armazenamento de versão de linha em vez de serem acessados diretamente. A falta de bloqueios compartilhados pode melhorar drasticamente a simultaneidade eliminando conflitos com transações simultâneas que procuram adquirir bloqueios incompatíveis. Essa vantagem é comumente resumida dizendo que os leitores não bloqueiam os escritores no RCSI e vice-versa. Como consequência adicional da redução do bloqueio devido a solicitações de bloqueio incompatíveis, a oportunidade para impasses geralmente é bastante reduzido quando executado em RCSI.
No entanto, esses benefícios não vêm sem custos e ressalvas . Por um lado, manter versões de linhas confirmadas consome recursos do sistema, por isso é importante que o ambiente físico esteja configurado para lidar com isso, principalmente em termos de tempdb requisitos de desempenho e memória/espaço em disco.
A segunda advertência é um pouco mais sutil:o RCSI fornece uma visão instantânea dos dados confirmados como eram no início da instrução, mas não há nada que impeça que os dados reais sejam alterados (e essas alterações confirmadas) enquanto a instrução RCSI está em execução. Não há bloqueios compartilhados, lembre-se. Uma consequência imediata deste segundo ponto é que o código T-SQL executado em RCSI pode tomar decisões com base em informações desatualizadas , em comparação com o estado atual confirmado do banco de dados. Falaremos mais sobre isso em breve.
Há uma última observação (específica da implementação) que quero fazer sobre o RCSI antes de prosseguirmos. Funções escalares e de várias instruções execute usando um contexto T-SQL interno diferente da instrução que o contém. Isso significa que a visualização point-in-time vista dentro de uma chamada de função escalar ou multi-instrução pode ser posterior à visualização point-in-time vista pelo restante da instrução. Isso pode resultar em inconsistências inesperadas, pois diferentes partes da mesma instrução veem dados de diferentes momentos . Esse comportamento estranho e confuso não se aplicam a funções in-line, que veem o mesmo instantâneo que a instrução em que aparecem.
Leituras e fantasmas não repetíveis
Dada uma visão pontual de nível de instrução do estado confirmado do banco de dados, pode não ser imediatamente aparente como uma transação confirmada de leitura em RCSI pode experimentar os fenômenos de leitura não repetível ou linha fantasma. De fato, se limitarmos nosso pensamento ao escopo de uma única declaração , nenhum desses fenômenos é possível no RCSI.
Ler os mesmos dados várias vezes na mesma instrução sob RCSI sempre retornará os mesmos valores de dados, nenhum dado desaparecerá entre essas leituras e nenhum dado novo aparecerá também. Se você está se perguntando que tipo de instrução pode ler os mesmos dados mais de uma vez, pense em consultas que fazem referência à mesma tabela mais de uma vez, talvez em uma subconsulta.
A consistência de leitura em nível de instrução é uma consequência óbvia das leituras emitidas em relação a um instantâneo fixo dos dados. A razão pela qual o RCSI não fornecer proteção contra leituras e fantasmas não repetíveis é que esses fenômenos do padrão SQL são definidos no nível da transação. Várias instruções dentro de uma transação em execução no RCSI podem ver dados diferentes, porque cada instrução vê uma visualização pontual a partir do momento essa instrução específica começado.
Para resumir, cada declaração dentro de uma transação RCSI vê um conjunto de dados estático confirmado, mas esse conjunto pode mudar entre as instruções dentro da mesma transação.
Dados desatualizados
A possibilidade de nosso código T-SQL tomar uma decisão importante com base em informações desatualizadas é mais do que um pouco inquietante. Considere por um momento que o instantâneo point-in-time usado por uma única instrução em execução no RCSI pode ser arbitrariamente antigo .
Uma instrução executada por um período considerável de tempo continuará a ver o estado confirmado do banco de dados como era quando a instrução começou. Enquanto isso, a instrução não contém todas as alterações confirmadas que ocorreram no banco de dados desde aquele momento.
Isso não quer dizer que os problemas associados ao acesso a dados obsoletos em RCSI sejam limitados a de longa duração declarações, mas as questões certamente podem ser mais pronunciadas em tais casos.
Uma questão de tempo
Esse problema de dados desatualizados se aplica a todas as declarações RCSI em princípio, não importa a rapidez com que elas possam ser concluídas. Por menor que seja a janela de tempo, sempre há uma chance de que uma operação simultânea modifique o conjunto de dados com o qual estamos trabalhando, sem que estejamos cientes dessa alteração. Vejamos novamente um dos exemplos simples que usamos antes ao explorar o comportamento do bloqueio de leitura confirmada:
INSERT dbo.OverdueInvoices SELECT I.InvoiceNumber FROM dbo.Invoices AS I WHERE I.TotalDue > ( SELECT SUM(P.Amount) FROM dbo.Payments AS P WHERE P.InvoiceNumber = I.InvoiceNumber );
Quando executado em RCSI, esta declaração não pode veja quaisquer modificações de banco de dados confirmadas que ocorram depois que a instrução começar a ser executada. Embora não encontremos os problemas de linhas perdidas ou múltiplas encontradas na implementação de bloqueio, uma transação simultânea pode adicionar um pagamento que deveria para evitar que um cliente receba uma carta de advertência severa sobre um pagamento em atraso após o início da execução da declaração acima.
Você provavelmente pode pensar em muitos outros problemas potenciais que podem ocorrer neste cenário, ou em outros que são conceitualmente semelhantes. Quanto mais tempo a instrução for executada, mais desatualizada sua visão do banco de dados se tornará e maior será o escopo para consequências possivelmente não intencionais.
Claro, existem muitos fatores atenuantes neste exemplo específico. O comportamento pode muito bem ser visto como perfeitamente aceitável. Afinal, enviar uma carta de lembrete porque um pagamento chegou alguns segundos atrasado é uma ação facilmente defendida. No entanto, o princípio permanece.
Falhas de regras comerciais e riscos de integridade
Problemas mais sérios podem surgir com o uso de informações desatualizadas do que enviar uma carta de advertência alguns segundos antes. Um bom exemplo dessa classe de fraqueza pode ser visto com código de gatilho usado para impor uma regra de integridade que talvez seja muito complexa para ser aplicada com restrições de integridade referencial declarativa. Para ilustrar, considere o código a seguir, que usa um gatilho para impor uma variação de uma restrição de chave estrangeira, mas que impõe o relacionamento apenas para determinadas linhas da tabela filha:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Parent (ParentID integer PRIMARY KEY); GO CREATE TABLE dbo.Child ( ChildID integer IDENTITY PRIMARY KEY, ParentID integer NOT NULL, CheckMe bit NOT NULL ); GO CREATE TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END; GO -- Insert parent row #1 INSERT dbo.Parent (ParentID) VALUES (1);
Agora considere uma transação em execução em outra sessão (use outra janela do SSMS para isso se você estiver acompanhando) que exclui a linha pai #1, mas ainda não confirma:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION; DELETE FROM dbo.Parent WHERE ParentID = 1;
De volta à nossa sessão original, tentamos inserir uma linha filho (marcada) que faz referência a esse pai:
INSERT dbo.Child (ParentID, CheckMe) VALUES (1, 1);
O código do gatilho é executado, mas porque o RCSI vê apenas confirmado dados a partir do momento em que a instrução foi iniciada, ela ainda vê a linha pai (não a exclusão não confirmada) e a inserção é bem-sucedida !
A transação que excluiu a linha pai agora pode confirmar sua alteração com sucesso, deixando o banco de dados em um estado inconsistente state em termos de nossa lógica de trigger:
COMMIT TRANSACTION; SELECT P.* FROM dbo.Parent AS P; SELECT C.* FROM dbo.Child AS C;
Este é um exemplo simplificado, é claro, e que pode ser facilmente contornado usando os recursos de restrição integrados. Regras de negócios muito mais complexas e restrições de pseudo-integridade podem ser escritas dentro e fora dos gatilhos . O potencial de comportamento incorreto sob RCSI deve ser óbvio.
Comportamento de bloqueio e dados confirmados mais recentemente
Mencionei anteriormente que não é garantido que o código T-SQL se comporte da mesma maneira na leitura RCSI confirmada, como acontecia usando a implementação de bloqueio. O exemplo de código de gatilho anterior é uma boa ilustração disso, mas preciso enfatizar que o problema geral não se limita aos gatilhos .
O RCSI normalmente não é uma boa escolha para qualquer código T-SQL cuja correção dependa do bloqueio se existir uma alteração não confirmada simultânea. O RCSI também pode não ser a escolha certa se o código depender da leitura de atual dados confirmados, em vez dos dados confirmados mais recentes no momento em que a instrução foi iniciada. Essas duas considerações estão relacionadas, mas não são a mesma coisa.
Bloqueando leitura confirmada em RCSI
O SQL Server fornece uma maneira de solicitar bloqueio leia o commit quando o RCSI estiver habilitado, usando a dica de tabela
READCOMMITTEDLOCK
. Podemos modificar nosso gatilho para evitar os problemas mostrados acima adicionando esta dica à tabela que precisa do comportamento de bloqueio para funcionar corretamente:ALTER TRIGGER dbo.Child_AI ON dbo.Child AFTER INSERT AS BEGIN -- Child rows with CheckMe = true -- must have an associated parent row IF EXISTS ( SELECT ins.ParentID FROM inserted AS ins WHERE ins.CheckMe = 1 EXCEPT SELECT P.ParentID FROM dbo.Parent AS P WITH (READCOMMITTEDLOCK) -- NEW!! ) BEGIN RAISERROR ('Integrity violation!', 16, 1); ROLLBACK TRANSACTION; END END;
Com essa alteração em vigor, a tentativa de inserir os blocos de linha filho potencialmente órfãos até que a transação de exclusão seja confirmada (ou abortada). Se a exclusão for confirmada, o código do gatilho detectará a violação de integridade e gerará o erro esperado.
Identificando consultas que podem não funcionar corretamente sob RCSI é uma tarefa não trivial que pode exigir testes extensivos para acertar (e lembre-se de que esses problemas são bastante gerais e não se limitam ao código de acionamento!) Além disso, adicionar o
READCOMMITTEDLOCK
dica para cada tabela que precisa pode ser um processo tedioso e propenso a erros. Até que o SQL Server forneça uma opção de escopo mais amplo para solicitar a implementação de bloqueio quando necessário, ficaremos presos ao uso das dicas de tabela. Próxima vez
A próxima postagem desta série continua nossa análise do isolamento de instantâneos com confirmação de leitura, com uma análise do comportamento surpreendente das instruções de modificação de dados no RCSI.
[ Veja o índice para toda a série ]