TL; DR: A pergunta abaixo se resume a: Ao inserir uma linha, existe uma janela de oportunidade entre a geração de um novo Identity
valor e o bloqueio da chave de linha correspondente no índice em cluster, onde um observador externo pode ver um novo Identity
valor inserido por uma transação simultânea? (No SQL Server.)
Versão detalhada
Eu tenho uma tabela do SQL Server com uma Identity
coluna chamada CheckpointSequence
, que é a chave do índice clusterizado da tabela (que também possui vários índices adicionais não clusterizados). As linhas são inseridas na tabela por vários processos e encadeamentos simultâneos (no nível de isolamento READ COMMITTED
e sem IDENTITY_INSERT
). Ao mesmo tempo, há processos que lêem periodicamente as linhas do índice em cluster, ordenadas por essa CheckpointSequence
coluna (também no nível de isolamento READ COMMITTED
, com a READ COMMITTED SNAPSHOT
opção desativada).
Atualmente, confio no fato de que os processos de leitura nunca podem "pular" um ponto de verificação. Minha pergunta é: Posso confiar nessa propriedade? E se não, o que eu poderia fazer para torná-lo realidade?
Exemplo: quando linhas com os valores de identidade 1, 2, 3, 4 e 5 são inseridas, o leitor não deve ver a linha com o valor 5 antes de ver a com o valor 4. Os testes mostram que a consulta, que contém uma ORDER BY CheckpointSequence
cláusula ( e uma WHERE CheckpointSequence > -1
cláusula), bloqueia de forma confiável sempre que a linha 4 for lida, mas ainda não confirmada, mesmo que a linha 5 já tenha sido confirmada.
Acredito que, pelo menos em teoria, pode haver uma condição de corrida aqui que possa causar essa suposição. Infelizmente, a documentação Identity
não diz muito sobre como Identity
funciona no contexto de várias transações simultâneas, apenas diz "Cada novo valor é gerado com base na atual semente e incremento". e "Cada novo valor para uma transação específica é diferente de outras transações simultâneas na tabela". ( MSDN )
Meu raciocínio é que ele deve funcionar de alguma forma assim:
- Uma transação é iniciada (explícita ou implicitamente).
- Um valor de identidade (X) é gerado.
- O bloqueio de linha correspondente é obtido no índice clusterizado com base no valor da identidade (a menos que a escalação do bloqueio seja iniciada, nesse caso, a tabela inteira está bloqueada).
- A linha é inserida.
- A transação é confirmada (possivelmente muito tempo depois), portanto, o bloqueio é removido novamente.
Eu acho que entre os passos 2 e 3, há uma janela muito pequena onde
- uma sessão simultânea pode gerar o próximo valor de identidade (X + 1) e executar todas as etapas restantes,
- permitindo assim que um leitor que esteja exatamente nesse ponto do tempo leia o valor X + 1, perdendo o valor de X.
Obviamente, a probabilidade disso parece extremamente baixa; mas ainda assim - isso poderia acontecer. Ou poderia?
(Se você estiver interessado no contexto: Esta é a implementação do SQL Persistence Engine da NEventStore. O NEventStore implementa um armazenamento de eventos somente para acréscimos, onde cada evento recebe um novo número de sequência de ponto de verificação crescente. para realizar cálculos de todos os tipos. Depois que um evento com ponto de verificação X for processado, os clientes consideram apenas eventos "mais recentes", ou seja, eventos com ponto de verificação X + 1 e acima. Portanto, é vital que os eventos nunca possam ser ignorados, como eles nunca seriam considerados novamente. No momento, estou tentando determinar se a Identity
implementação do ponto de verificação com base em conformidade com esse requisito. Estas são as instruções SQL exatas usadas : Esquema , consulta do Writer ,Consulta do leitor .)
Se eu estiver certo e a situação descrita acima puder surgir, posso ver apenas duas opções para lidar com elas, ambas insatisfatórias:
- Ao visualizar um valor de sequência de ponto de verificação X + 1 antes de ver X, descarte X + 1 e tente novamente mais tarde. No entanto, como
Identity
é claro que pode produzir lacunas (por exemplo, quando a transação é revertida), X pode nunca chegar. - Portanto, mesma abordagem, mas aceite a diferença após n milissegundos. No entanto, que valor de n devo assumir?
Alguma ideia melhor?
fonte
Respostas:
Sim.
A alocação de valores de identidade é independente da transação do usuário que contém . Esse é um dos motivos pelos quais os valores de identidade são consumidos, mesmo que a transação seja revertida. A operação de incremento em si é protegida por uma trava para evitar corrupção, mas essa é a extensão das proteções.
Nas circunstâncias específicas de sua implementação, a alocação de identidade (uma chamada para
CMEDSeqGen::GenerateNewValue
) é feita antes que a transação do usuário para a inserção seja ativada (e, portanto, antes de qualquer bloqueio).Ao executar duas inserções simultaneamente com um depurador conectado para permitir o congelamento de um encadeamento logo após o valor da identidade ser incrementado e alocado, consegui reproduzir um cenário em que:
Após a etapa 3, uma consulta usando o número da linha sob bloqueio de leitura confirmada retornou o seguinte:
Na sua implementação, isso faria com que o Checkpoint ID 3 fosse ignorado incorretamente.
A janela de falta de oportunidade é relativamente pequena, mas existe. Para fornecer um cenário mais realista do que um depurador conectado: Um thread de consulta em execução pode gerar o agendador após a etapa 1 acima. Isso permite que um segundo encadeamento aloque um valor de identidade, insira e confirme, antes que o encadeamento original seja retomado para executar sua inserção.
Para maior clareza, não há bloqueios ou outros objetos de sincronização protegendo o valor da identidade depois que ele é alocado e antes de ser usado. Por exemplo, após a etapa 1 acima, uma transação simultânea pode ver o novo valor de identidade usando funções T-SQL como
IDENT_CURRENT
antes da linha existir na tabela (mesmo não confirmada).Fundamentalmente, não há mais garantias quanto aos valores de identidade do que o documentado :
É isso mesmo.
Se for necessário um processamento FIFO transacional estrito , você provavelmente não terá outra opção a não ser serializar manualmente. Se o aplicativo tiver requisitos menos específicos, você terá mais opções. A questão não é 100% clara a esse respeito. No entanto, você pode encontrar algumas informações úteis no artigo de Remus Rusanu, Usando tabelas como filas .
fonte
Como Paul White respondeu absolutamente correto, existe a possibilidade de linhas de identidade "puladas" temporariamente. Aqui está apenas um pequeno pedaço de código para reproduzir esse caso por conta própria.
Crie um banco de dados e uma tabela de teste:
Execute inserções simultâneas e selecione nesta tabela em um programa de console C #:
Esse console imprime uma linha para cada caso quando um dos segmentos de leitura "perde" uma entrada.
fonte
IDENTITY
produziria lacunas (como reverter uma transação), as linhas impressas realmente mostram os valores "ignorados" (ou pelo menos o fizeram quando executei e verifiquei na minha máquina). Amostra de reprodução muito boa!É melhor não esperar que as identidades sejam consecutivas, pois existem muitos cenários que podem deixar lacunas. É melhor considerar a identidade como um número abstrato e não anexar nenhum significado comercial a ela.
Basicamente, as lacunas podem ocorrer se você reverter as operações INSERT (ou excluir explicitamente as linhas) e duplicatas podem ocorrer se você definir a propriedade da tabela IDENTITY_INSERT como ON.
Lacunas podem ocorrer quando:
A propriedade de identidade em uma coluna nunca garantiu:
• Singularidade
• Valores consecutivos em uma transação. Se os valores precisarem ser consecutivos, a transação deverá usar um bloqueio exclusivo na tabela ou o nível de isolamento SERIALIZABLE.
• Valores consecutivos após a reinicialização do servidor.
• Reutilização de valores.
Se você não puder usar valores de identidade por causa disso, crie uma tabela separada contendo um valor atual e gerencie o acesso à atribuição de tabela e número com seu aplicativo. Isso tem o potencial de afetar o desempenho.
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx
fonte
ORDER BY CheckpointSequence
cláusula (que por acaso é a ordem do índice em cluster). Acho que tudo se resume à questão de saber se a geração de um valor de identidade está de alguma forma vinculada aos bloqueios executados pela instrução INSERT ou se essas são simplesmente duas ações não relacionadas executadas pelo SQL Server uma após a outra.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. Acho que essa consulta não passaria da linha bloqueada 4 ou seria? (Em meus experimentos, ele bloqueia quando a consulta tenta adquirir o bloqueio do teclado para a linha 4.)Suspeito que ocasionalmente possa causar problemas, problemas que pioram quando o servidor está sob carga pesada. Considere duas transações:
No cenário acima, seu LAST_READ_ID será 6, portanto, 5 nunca serão lidos.
fonte
Executando este script:
Abaixo estão os bloqueios que vejo adquiridos e liberados como capturados por uma sessão de evento estendido:
Observe o bloqueio RI_N KEY adquirido imediatamente antes do bloqueio da tecla X para a nova linha que está sendo criada. Esse bloqueio de curta duração impedirá que uma inserção simultânea adquira outro bloqueio RI_N KEY, pois os bloqueios RI_N são incompatíveis. A janela que você mencionou entre as etapas 2 e 3 não é uma preocupação porque o bloqueio de intervalo é adquirido antes do bloqueio de linha na chave recém-gerada.
Desde que você
SELECT...ORDER BY
inicie a verificação antes das linhas recém-inseridas desejadas, esperaria o comportamento desejado noREAD COMMITTED
nível de isolamento padrão , desde que aREAD_COMMITTED_SNAPSHOT
opção do banco de dados esteja desativada.fonte
RangeI_N
são compatíveis , ou seja, não se bloqueiam (o bloqueio geralmente existe para bloquear em um leitor serializável existente).Pelo meu entendimento do SQL Server, o comportamento padrão é que a segunda consulta não exiba nenhum resultado até que a primeira consulta tenha sido confirmada. Se a primeira consulta executar um ROLLBACK em vez de um COMMIT, haverá um ID ausente na sua coluna.
Configuração básica
Tabela de banco de dados
Criei uma tabela de banco de dados com a seguinte estrutura:
Nível de isolamento do banco de dados
Eu verifiquei o nível de isolamento do meu banco de dados com a seguinte instrução:
O qual retornou o seguinte resultado para meu banco de dados:
(Essa é a configuração padrão para um banco de dados no SQL Server 2012)
Scripts de teste
Os scripts a seguir foram executados usando as configurações padrão do cliente SSMS do SQL Server e as configurações padrão do SQL Server.
Configurações de conexões do cliente
O cliente foi configurado para usar o Nível de isolamento de transação
READ COMMITTED
conforme as Opções de consulta no SSMS.Consulta 1
A consulta a seguir foi executada em uma janela de consulta com o SPID 57
Consulta 2
A consulta a seguir foi executada em uma janela de consulta com o SPID 58
A consulta não é concluída e está aguardando o lançamento do bloqueio eXclusive em uma PAGE.
Script para determinar o bloqueio
Este script exibe o bloqueio que ocorre nos objetos de banco de dados para as duas transações:
E aqui estão os resultados:
Os resultados mostram que a janela de consulta um (SPID 57) possui um bloqueio compartilhado (S) no DATABASE, um bloqueio eXlusive (IX) pretendido no OBJECT, um bloqueio eXlusive (IX) pretendido na página que deseja inserir e um eXclusivo O bloqueio (X) na KEY está inserido, mas ainda não foi confirmado.
Devido aos dados não confirmados, a segunda consulta (SPID 58) possui um bloqueio compartilhado (S) no nível DATABASE, um bloqueio de compartilhamento compartilhado (IS) no OBJECT, um bloqueio de compartilhamento compartilhado (IS) na página ) trava na KEY com um status de solicitação WAIT.
Sumário
A consulta na primeira janela de consulta é executada sem confirmação. Como a segunda consulta pode apenas
READ COMMITTED
dados, aguarda até que o tempo limite ocorra ou até que a transação tenha sido confirmada na primeira consulta.Isso é do meu entendimento do comportamento padrão do Microsoft SQL Server.
Você deve observar que o ID está realmente em sequência para leituras subsequentes pelas instruções SELECT, se a primeira instrução COMMITs.
Se a primeira instrução executar um ROLLBACK, você encontrará um ID ausente na sequência, mas ainda com o ID em ordem crescente (desde que você tenha criado o INDEX com a opção padrão ou ASC na coluna ID).
Atualizar:
(Sem rodeios) Sim, você pode confiar na coluna de identidade funcionando corretamente, até encontrar um problema. Há apenas um HOTFIX referente ao SQL Server 2000 e a coluna de identidade no site da Microsoft.
Se você não pudesse confiar na atualização correta da coluna de identidade, acho que haveria mais hotfixes ou patches no site da Microsoft.
Se você possui um contrato de suporte da Microsoft, sempre pode abrir um caso consultivo e solicitar informações adicionais.
fonte
Identity
valor e a aquisição do bloqueio KEY na linha (onde poderiam ocorrer leituras / gravadores simultâneos). Não acho que isso seja provado impossível por suas observações, porque não se pode parar a execução da consulta e analisar os bloqueios durante esse período de tempo muito curto.