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

Modificações de dados em isolamento de instantâneos confirmados de leitura

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

A postagem anterior desta série mostrou como uma instrução T-SQL executada sob isolamento de instantâneo de leitura confirmada (RCSI ) normalmente vê uma visualização de instantâneo do estado confirmado do banco de dados como era quando a instrução iniciou a execução. Essa é uma boa descrição de como as coisas funcionam para instruções que leem dados, mas existem diferenças importantes para instruções executadas em RCSI que modificam linhas existentes .

Destaco a modificação de linhas existentes acima, porque as seguintes considerações se aplicam apenas a UPDATE e DELETE operações (e as ações correspondentes de um MERGE demonstração). Para ser claro, INSERT declarações são especificamente excluídas do comportamento que estou prestes a descrever porque as inserções não modificam existentes dados.

Atualizar bloqueios e versões de linha


A primeira diferença é que as instruções de atualização e exclusão não leem versões de linha em RCSI ao pesquisar as linhas de origem a serem modificadas. Atualizar e excluir instruções no RCSI, em vez disso, adquirem bloqueios de atualização ao pesquisar linhas qualificadas. O uso de bloqueios de atualização garante que a operação de pesquisa encontre linhas para modificar usando os dados confirmados mais recentes .

Sem bloqueios de atualização, a pesquisa seria baseada em uma versão possivelmente desatualizada do conjunto de dados (dados confirmados como estavam quando a instrução de modificação de dados foi iniciada). Isso pode lembrá-lo do exemplo de gatilho que vimos da última vez, em que um READCOMMITTEDLOCK dica foi usada para reverter de RCSI para a implementação de bloqueio de isolamento de leitura confirmada. Essa dica foi necessária nesse exemplo para evitar basear uma ação importante em informações desatualizadas. O mesmo tipo de raciocínio está sendo usado aqui. Uma diferença é que o READCOMMITTEDLOCK dica adquire bloqueios compartilhados em vez de bloqueios de atualização. Além disso, o SQL Server adquire automaticamente bloqueios de atualização para proteger as modificações de dados em RCSI sem exigir que adicionemos uma dica explícita.

Fazer bloqueios de atualização também garante que a instrução de atualização ou exclusão bloqueie se encontrar um bloqueio incompatível, por exemplo, um bloqueio exclusivo protegendo uma modificação de dados em andamento realizada por outra transação simultânea.

Uma complicação adicional é que o comportamento modificado só se aplica para a tabela que é o destino da operação de atualização ou exclusão. Outras tabelas no mesmo declaração de exclusão ou atualização, incluindo referências adicionais para a tabela de destino, continue a usar versões de linha .

Alguns exemplos provavelmente são necessários para tornar esses comportamentos confusos um pouco mais claros…

Configuração do teste


O script a seguir garante que estamos todos configurados para usar RCSI, cria uma tabela simples e adiciona duas linhas de exemplo a ela:
ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    Data integer NOT NULL
);
GO
INSERT dbo.Test
    (RowID, Data)
VALUES 
    (1, 1234),
    (2, 2345);

A próxima etapa precisa ser executada em uma sessão separada . Ele inicia uma transação e exclui as duas linhas da tabela de teste (parece estranho, mas tudo isso fará sentido em breve):
BEGIN TRANSACTION;
DELETE dbo.Test 
WHERE RowID IN (1, 2);

Observe que a transação é deliberadamente deixada aberta . Isso mantém bloqueios exclusivos em ambas as linhas que estão sendo excluídas (junto com os bloqueios exclusivos de intenção usuais na página que o contém e na própria tabela), pois a consulta abaixo pode ser usada para mostrar:
SELECT
    resource_type,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE 
    request_session_id = @@SPID;


O teste de seleção


Voltando à sessão original , a primeira coisa que quero mostrar é que as instruções select regulares usando RCSI ainda veem as duas linhas sendo excluídas. A consulta de seleção abaixo usa versões de linha para retornar os dados confirmados mais recentes no momento em que a instrução é iniciada:
SELECT *
FROM dbo.Test;



Caso isso pareça surpreendente, lembre-se de que mostrar as linhas como excluídas significaria exibir uma exibição não confirmada dos dados, o que não é permitido no isolamento de leitura confirmada.

O teste de exclusão


Apesar do sucesso do teste de seleção, uma tentativa de excluir essas mesmas linhas da sessão atual serão bloqueadas. Você pode imaginar que esse bloqueio ocorre quando a operação tenta adquirir exclusivo fechaduras, mas não é o caso.

A exclusão não usa controle de versão de linha localizar as linhas a serem excluídas; ele tenta adquirir bloqueios de atualização em vez disso. Os bloqueios de atualização são incompatíveis com os bloqueios de linha exclusivos mantidos pela sessão com a transação aberta, portanto, a consulta bloqueia:
DELETE dbo.Test 
WHERE RowID IN (1, 2);

O plano de consulta estimado para esta instrução mostra que as linhas a serem excluídas são identificadas por uma operação de busca regular antes que um operador separado execute a exclusão real:



Podemos ver os bloqueios mantidos neste estágio executando a mesma consulta de bloqueio de antes (de outra sessão) lembrando de alterar a referência SPID para aquela usada pela consulta bloqueada. Os resultados ficam assim:



Nossa consulta de exclusão está bloqueada no operador Clustered Index Seek, que está aguardando para adquirir um bloqueio de atualização para ler dados. Isso mostra que localizar as linhas a serem excluídas em RCSI adquire bloqueios de atualização em vez de ler dados com versão potencialmente obsoletos. Também mostra que o bloqueio não é devido à parte de exclusão da operação esperando para adquirir um bloqueio exclusivo.

O teste de atualização


Cancele a consulta bloqueada e tente a seguinte atualização:
UPDATE dbo.Test
SET Data = Data + 1000
WHERE RowID IN (1, 2);

O plano de execução estimado é semelhante ao visto no teste de exclusão:



O Compute Scalar está lá para determinar o resultado da adição de 1000 ao valor atual da coluna Data em cada linha, que é lida pelo Clustered Index Seek. Esta declaração também bloqueia quando executado, devido ao bloqueio de atualização solicitado pela operação de leitura. A captura de tela abaixo mostra os bloqueios mantidos quando a consulta é bloqueada:



Como antes, a consulta é bloqueada na busca, esperando que o bloqueio exclusivo incompatível seja liberado para que um bloqueio de atualização possa ser adquirido.

O teste de inserção


O próximo teste apresenta uma instrução que insere uma nova linha em nossa tabela de teste, usando o valor da coluna Dados da linha existente com ID 1 na tabela. Lembre-se de que esta linha ainda está bloqueada exclusivamente por sessão com a transação aberta:
INSERT dbo.Test
    (RowID, Data)
SELECT 3, Data
FROM dbo.Test
WHERE RowID = 1;

O plano de execução é novamente semelhante aos testes anteriores:



Desta vez, a consulta não está bloqueada . Isso mostra que os bloqueios de atualização não foram adquiridos durante a leitura dados para a inserção. Em vez disso, essa consulta usou o controle de versão de linha para adquirir o valor da coluna Dados para a linha recém-inserida. Os bloqueios de atualização não foram adquiridos porque esta instrução não localizou nenhuma linha para modificar , ele apenas lê os dados a serem usados ​​na inserção.

Podemos ver essa nova linha na tabela usando a consulta de teste de seleção de antes:



Observe que somos capaz de atualizar e excluir a nova linha (o que exigirá bloqueios de atualização) porque não há bloqueio exclusivo conflitante. A sessão com a transação aberta só possui bloqueios exclusivos nas linhas 1 e 2:
-- Update the new row
UPDATE dbo.Test
SET Data = 9999
WHERE RowID = 3;
-- Show the data
SELECT * FROM dbo.Test;
-- Delete the new row
DELETE dbo.Test
WHERE RowID = 3;



Este teste confirma que instruções de inserção não adquirem bloqueios de atualização ao ler , porque, diferentemente das atualizações e exclusões, elas não modificam uma linha existente. A parte de leitura de uma inserção A instrução usa o comportamento normal de controle de versão de linha RCSI.

Teste de referência múltipla


Mencionei antes que apenas a referência de tabela única usada para localizar linhas para modificar adquire bloqueios de atualização; outras tabelas na mesma instrução update ou delete ainda lêem versões de linha. Como um caso especial desse princípio geral, uma instrução de modificação de dados com várias referências à mesma tabela aplica bloqueios de atualização apenas em uma instância usado para localizar linhas para modificar. Este teste final ilustra esse comportamento mais complexo, passo a passo.

A primeira coisa que precisamos é de uma nova terceira linha para nossa tabela de teste, desta vez com um zero na coluna Data:
INSERT dbo.Test
    (RowID, Data)
VALUES
    (3, 0);

Como esperado, essa inserção prossegue sem bloqueio, resultando em uma tabela parecida com esta:



Lembre-se, a segunda sessão ainda é exclusiva trava nas linhas 1 e 2 neste ponto. Somos livres para adquirir bloqueios na linha 3, se necessário. A consulta a seguir é a que usaremos para mostrar o comportamento com várias referências à tabela de destino:
-- Multi-reference update test
UPDATE WriteRef
SET Data = ReadRef.Data * 2
OUTPUT 
    ReadRef.RowID, 
    ReadRef.Data,
    INSERTED.RowID AS UpdatedRowID,
    INSERTED.Data AS NewDataValue
FROM dbo.Test AS ReadRef
JOIN dbo.Test AS WriteRef
    ON WriteRef.RowID = ReadRef.RowID + 2
WHERE 
    ReadRef.RowID = 1;

Esta é uma consulta mais complexa, mas sua operação é relativamente simples. Existem duas referências à tabela de teste, uma que eu apelidei como ReadRef e a outra como WriteRef. A ideia é ler da linha 1 (usando uma versão de linha) via ReadRef e para atualizar a terceira linha (que precisará de um bloqueio de atualização) usando WriteRef.

A consulta especifica a linha 1 explicitamente na cláusula where para a referência da tabela de leitura. Ele se une à referência de escrita à mesma tabela adicionando 2 a esse RowID (identificando assim a linha 3). A instrução de atualização também usa uma cláusula de saída para retornar um conjunto de resultados que mostra os valores lidos da tabela de origem e as alterações resultantes feitas na linha 3.

O plano de consulta estimado para esta declaração é o seguinte:



As propriedades da busca rotulada (1) mostre que esta busca está no ReadRef alias, lendo dados da linha com RowID 1:



Esta operação de busca não localiza uma linha que será atualizada, portanto, os bloqueios de atualização não levado; a leitura é realizada usando dados versionados. A leitura não é bloqueada pelos bloqueios exclusivos mantidos pela outra sessão.

O escalar de computação rotulado (2) define uma expressão rotulada 1004 que calcula o valor atualizado da coluna Data. A expressão 1009 calcula o ID da linha a ser atualizado (1 + 2 =ID da linha 3):



A segunda busca é uma referência à mesma tabela (3). Essa busca localiza a linha que será atualizada (linha 3) usando a expressão 1009:



Como essa busca localiza uma linha a ser alterada, um bloqueio de atualização é obtido em vez de usar versões de linha. Não há bloqueio exclusivo conflitante no ID da linha 3, portanto, a solicitação de bloqueio é concedida imediatamente.

O operador final destacado (4) é a própria operação de atualização. O bloqueio de atualização na linha 3 é atualizado para um exclusivo lock neste ponto, pouco antes da modificação ser realmente executada. Este operador também retorna os dados especificados na cláusula de saída da declaração de atualização:



O resultado da instrução de atualização (gerada pela cláusula de saída) é mostrado abaixo:



O estado final da tabela é mostrado abaixo:



Podemos confirmar os bloqueios realizados durante a execução usando um rastreamento do Profiler:



Isso mostra que apenas uma única atualização bloqueio de tecla de linha é adquirido. Quando esta linha atinge o operador de atualização, o bloqueio é convertido em um exclusivo trancar. No final da instrução, o bloqueio é liberado.

Você pode ver na saída de rastreamento que o valor de hash de bloqueio para a linha bloqueada por atualização é (98ec012aa510) no meu banco de dados de teste. A consulta a seguir mostra que esse hash de bloqueio está realmente associado ao RowID 3 no índice clusterizado:
SELECT RowID, %%LockRes%%
FROM dbo.Test;



Observe que os bloqueios de atualização obtidos nesses exemplos têm vida útil mais curta do que os bloqueios de atualização obtidos se especificarmos um UPDLOCK dica. Esses bloqueios de atualização internos são liberados no final da instrução, enquanto UPDLOCK bloqueios são mantidos até o final da transação.

Isso conclui a demonstração de casos em que o RCSI adquire bloqueios de atualização para ler os dados confirmados atuais em vez de usar o controle de versão de linha.

Bloqueios compartilhados e de intervalo de chaves em RCSI


Há vários outros cenários em que o mecanismo de banco de dados ainda pode adquirir bloqueios em RCSI. Todas essas situações estão relacionadas à necessidade de preservar a exatidão que seria ameaçada por confiar em dados com versão potencialmente desatualizados.

Bloqueios compartilhados obtidos para validação de chave estrangeira


Para duas tabelas em um relacionamento direto de chave estrangeira, o mecanismo de banco de dados precisa tomar medidas para garantir que as restrições não sejam violadas, contando com leituras com versão potencialmente obsoletas. A implementação atual faz isso mudando para bloqueio de leitura confirmada ao acessar dados como parte de uma verificação automática de chave estrangeira.

A obtenção de bloqueios compartilhados garante que a verificação de integridade leia os dados confirmados mais recentes (não uma versão antiga) ou bloqueie devido a uma modificação simultânea em andamento. A mudança para bloqueio de leitura confirmada só se aplica ao método de acesso específico usado para verificar dados de chave estrangeira; outro acesso a dados na mesma instrução continua a usar versões de linha.

Esse comportamento se aplica apenas a instruções que alteram dados, onde a alteração afeta diretamente um relacionamento de chave estrangeira. Para modificações na tabela referenciada (pai), isso significa atualizações que afetam o valor referenciado (a menos que esteja definido como NULL ) e todas as exclusões. Para a tabela de referência (filho), isso significa todas as inserções e atualizações (novamente, a menos que a referência de chave seja NULL ). As mesmas considerações se aplicam aos efeitos do componente de um MERGE .

Um exemplo de plano de execução mostrando uma pesquisa de chave estrangeira que usa bloqueios compartilhados é mostrado abaixo:


Serializável para chaves estrangeiras em cascata


Onde o relacionamento de chave estrangeira tem uma ação em cascata, a correção requer uma escalação local para semântica de isolamento serializável. Isso significa que você verá bloqueios de intervalo de teclas para uma ação referencial em cascata. Como foi o caso dos bloqueios de atualização vistos anteriormente, esses bloqueios de intervalo de chaves têm como escopo a instrução, não a transação. Um exemplo de plano de execução mostrando onde os bloqueios serializáveis ​​internos são obtidos no RCSI é mostrado abaixo:


Outros cenários


Existem muitos outros casos específicos em que o mecanismo estende automaticamente a vida útil dos bloqueios ou escala localmente o nível de isolamento para garantir a exatidão. Isso inclui a semântica serializável usada ao manter uma exibição indexada relacionada ou ao manter um índice que tem o IGNORE_DUP_KEY conjunto de opções.

A mensagem principal é que o RCSI reduz a quantidade de bloqueio, mas nem sempre pode eliminá-lo completamente.

Próxima vez


A próxima postagem desta série analisa o nível de isolamento do instantâneo.
[ Veja o índice para toda a série ]