TL;DR: A questão 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 da linha correspondente no índice clusterizado, onde um observador externo poderia ver uma nova Identity
valor inserido por uma transação concorrente? (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 não clusterizados adicionais). As linhas são inseridas na tabela por vários processos e threads simultâneos (no nível de isolamento READ COMMITTED
e sem IDENTITY_INSERT
). Ao mesmo tempo, existem processos que leem periodicamente as linhas do índice clusterizado, ordenadas por aquela CheckpointSequence
coluna (também em 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. A minha dúvida é: Posso contar com este imóvel? E se não, o que eu poderia fazer para torná-lo verdade?
Exemplo: Ao inserir linhas com valores de identidade 1, 2, 3, 4 e 5, o leitor não deve ver a linha com valor 5 antes de ver a linha com 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 deve ser 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 pode fazer com que essa suposição seja quebrada. 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 semente e incremento atuais". e "Cada novo valor para uma determinada transação é diferente de outras transações simultâneas na tabela." ( MSDN )
Meu raciocínio é que 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 de identidade (a menos que o escalonamento de bloqueio seja ativado, caso em que toda a tabela é bloqueada).
- A linha é inserida.
- A transação é confirmada (possivelmente muito tempo depois), então o bloqueio é removido novamente.
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 vindo exatamente naquele ponto do tempo leia o valor X+1, perdendo o valor de X.
Claro, a probabilidade disso parece extremamente baixa; mas ainda assim - isso pode acontecer. Ou poderia?
(Se você estiver interessado no contexto: esta é a implementação do SQL Persistence Engine do NEventStore. O NEventStore implementa um armazenamento de evento apenas anexado, onde cada evento obtém um novo número de sequência de ponto de verificação ascendente. Os clientes leem os eventos do armazenamento de eventos ordenados por ponto de verificação para realizar cálculos de todos os tipos. Depois que um evento com ponto de verificação X é 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 atende a esse requisito. Estas são as instruções SQL exatas usadas : Schema , Writer's query ,Pergunta do Leitor .)
Se eu estiver certo e a situação descrita acima puder surgir, vejo apenas duas opções de lidar com eles, ambas insatisfatórias:
- Ao ver um valor de sequência de ponto de verificação X+1 antes de ter visto X, descarte X+1 e tente novamente mais tarde. No entanto, porque
Identity
é claro que pode produzir lacunas (por exemplo, quando a transação é revertida), X pode nunca vir. - Então, mesma abordagem, mas aceite o intervalo após n milissegundos. No entanto, que valor de n devo assumir?
Alguma ideia melhor?
Sim.
A alocação de valores de identidade é independente da transação do usuário recipiente . Esse é um dos motivos pelos quais os valores de identidade são consumidos mesmo se a transação for revertida. A própria operação de incremento é 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 que qualquer bloqueio seja feito).Ao executar duas inserções simultaneamente com um depurador anexado para permitir o congelamento de um thread 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 row_number sob bloqueio de leitura confirmada retornou o seguinte:
Em sua implementação, isso resultaria no fato de o Checkpoint ID 3 ser ignorado incorretamente.
A janela de má oportunidade é relativamente pequena, mas existe. Para fornecer um cenário mais realista do que ter um depurador anexado: Um thread de consulta em execução pode gerar o agendador após a etapa 1 acima. Isso permite que um segundo thread aloque um valor de identidade, insira e confirme, antes que o thread original continue a executar sua inserção.
Para maior clareza, não há bloqueios ou outros objetos de sincronização protegendo o valor de 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 de a linha existir na tabela (mesmo não confirmada).Fundamentalmente, não há mais garantias em torno dos valores de identidade do que documentado :
Realmente é isso.
Se o processamento FIFO transacional estrito for necessário, você provavelmente não terá escolha a não ser serializar manualmente. Se o aplicativo tiver requisitos menos onerosos, você terá mais opções. A questão não está 100% clara a esse respeito. No entanto, você pode encontrar algumas informações úteis no artigo de Remus Rusanu Using Tables as Queues .
Como Paul White respondeu absolutamente correto, existe a possibilidade de linhas de identidade temporariamente "puladas". Aqui está apenas um pequeno pedaço de código para reproduzir este caso por conta própria.
Crie um banco de dados e uma tabela de teste:
Execute inserções e seleções simultâneas nesta tabela em um programa de console C#:
Este console imprime uma linha para cada caso quando um dos threads de leitura "perde" uma entrada.
É melhor não esperar que as identidades sejam consecutivas porque há muitos cenários que podem deixar lacunas. É melhor considerar a identidade como um número abstrato e não atribuir nenhum significado comercial a ela.
Basicamente, lacunas podem ocorrer se você reverter operações INSERT (ou excluir linhas explicitamente) e duplicatas podem ocorrer se você definir a propriedade da tabela IDENTITY_INSERT como ON.
As lacunas podem ocorrer quando:
A propriedade de identidade em uma coluna nunca foi garantida:
• Singularidade
• Valores consecutivos dentro de uma transação. Se os valores devem ser consecutivos, a transação deve usar um bloqueio exclusivo na tabela ou usar o nível de isolamento SERIALIZABLE.
• Valores consecutivos após a reinicialização do servidor.
• Reaproveitamento 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 à tabela e a atribuição de 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
Eu suspeito que ocasionalmente pode levar a 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, então 5 nunca será lido.
Executando este script:
Abaixo estão os bloqueios que vejo adquiridos e liberados conforme 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 intervalo 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.
Contanto que você
SELECT...ORDER BY
comece a verificação antes das linhas recém-inseridas desejadas, eu esperaria o comportamento que você deseja noREAD COMMITTED
nível de isolamento padrão, desde que aREAD_COMMITTED_SNAPSHOT
opção de banco de dados esteja desativada.Pelo que entendi do SQL Server, o comportamento padrão é que a segunda consulta não exiba nenhum resultado até que a primeira consulta seja confirmada. Se a primeira consulta fizer um ROLLBACK em vez de um COMMIT, você terá um ID ausente em 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 declaração:
Que retornou o seguinte resultado para meu banco de dados:
(Esta é 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 SQL Server SSMS 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
READ COMMITTED
de isolamento de transação de acordo com as opções de consulta no SSMS.Consulta 1
A consulta a seguir foi executada em uma janela 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 conclui e aguarda a liberação do bloqueio eXclusivo em uma PÁGINA.
Script para determinar o bloqueio
Este script exibe o bloqueio que ocorre nos objetos do 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 BANCO DE DADOS, um bloqueio Intencionado eXlusivo (IX) no OBJETO, um bloqueio Intencionado eXlusivo (IX) na PÁGINA que deseja inserir e um bloqueio eXclusivo bloqueio (X) na CHAVE que foi inserido, mas ainda não confirmado.
Por causa dos dados não confirmados, a segunda consulta (SPID 58) tem um bloqueio compartilhado (S) no nível DATABASE, um bloqueio compartilhado intencional (IS) no OBJECT, um bloqueio compartilhado compartilhado (IS) na página a compartilhado (S ) bloqueie a CHAVE com um status de solicitação WAIT.
Resumo
A consulta na primeira janela de consulta é executada sem confirmação. Como a segunda consulta só pode fornecer
READ COMMITTED
dados, ela espera até que o tempo limite ocorra ou até que a transação seja 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 por instruções SELECT se a primeira instrução for COMMIT.
Se a primeira instrução fizer 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 que a coluna de identidade está funcionando corretamente, até encontrar um problema. Existe apenas um HOTFIX em relação ao SQL Server 2000 e à 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ê tiver um contrato de suporte da Microsoft, sempre poderá abrir um caso consultivo e solicitar informações adicionais.