Gerenciando simultaneidade ao usar o padrão SELECT-UPDATE

25

Digamos que você tenha o seguinte código (ignore que é horrível):

BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else

A meu ver, isso NÃO está gerenciando a concorrência corretamente. Só porque você tem uma transação, não significa que alguém não lerá o mesmo valor que você fez antes de chegar à sua declaração de atualização.

Agora, deixando o código como está (percebo que isso é melhor tratado como uma única instrução ou ainda melhor usando uma coluna de incremento automático / identidade) - quais são as maneiras certas de fazê-lo lidar com a concorrência corretamente e evitar condições de corrida que permitem que dois clientes obtenham o mesmo valor de identificação?

Tenho certeza de que adicionar um WITH (UPDLOCK, HOLDLOCK)ao SELECT fará o truque. O nível de isolamento de transação SERIALIZABLE parece funcionar bem, pois nega que mais alguém leia o que você fez até o tran terminar ( UPDATE : isso é falso. Veja a resposta de Martin). Isso é verdade? Ambos irão funcionar igualmente bem? Um prefere o outro?

Imagine fazer algo mais legítimo do que uma atualização de ID - algum cálculo com base em uma leitura que você precisa atualizar. Pode haver muitas tabelas envolvidas, algumas das quais você escreverá e outras que não. Qual é a melhor prática aqui?

Depois de escrever essa pergunta, acho que as dicas de bloqueio são melhores porque você está bloqueando apenas as tabelas necessárias, mas eu apreciaria a opinião de qualquer pessoa.

PS E não, eu não sei a melhor resposta e realmente quero entender melhor! :)

ErikE
fonte
Apenas para esclarecimento: você deseja impedir que 2 clientes leiam o mesmo valor ou emitam updateque possam ser baseados em dados obsoletos? Neste último caso, você pode usar a rowversioncoluna para verificar se a linha a ser atualizada não foi alterada desde que foi lida.
a1ex07
Não queremos que um segundo cliente obtenha o valor do ID antigo antes de ser atualizado para o novo valor pelo primeiro cliente. Deve bloquear.
ErikE 12/02/12

Respostas:

11

Apenas abordando o SERIALIZABLEaspecto do nível de isolamento. Sim, isso funcionará, mas com risco de conflito.

Duas transações poderão ler a linha simultaneamente. Eles não se bloquearão, pois terão um Sbloqueio de objeto ou um bloqueio de índice RangeS-Sdependente da estrutura da tabela e esses bloqueios são compatíveis . Mas eles se bloquearão ao tentar adquirir os bloqueios necessários para a atualização ( IXbloqueio ou índice de objeto, RangeS-Urespectivamente), o que levará a um conflito.

O uso de uma UPDLOCKdica explícita serializará as leituras, evitando assim o risco de conflito.

Martin Smith
fonte
+1, mas: para tabelas de heap, você ainda pode obter um impasse de conversão, mesmo com os bloqueios de atualização: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/11/…
AK
Bizarro, @alex. Imagino que tem a ver com uma condição de corrida do motor tentando descobrir o que para travar antes de realmente UPDLOCKing-lo ...
ErikE
@ErikE - O conflito de conversão no artigo de Alex está sendo convertido de IXpara Xo próprio heap. Curiosamente, nenhuma linha é qualificada, portanto, nenhum bloqueio de linha é removido. Não sei por que é necessário o Xbloqueio.
Martin Smith
11

Eu acho que a melhor abordagem para você seria realmente expor seu módulo a alta concorrência e ver por si mesmo. Às vezes, o UPDLOCK sozinho é suficiente e não há necessidade do HOLDLOCK. Às vezes, o sp_getapplock funciona muito bem. Eu não faria nenhuma declaração geral aqui - às vezes, adicionar mais um índice, gatilho ou exibição indexada altera o resultado. Precisamos enfatizar o código do teste e ver por nós mesmos caso a caso.

Eu escrevi vários exemplos de testes de estresse aqui

Edit: para melhor conhecimento dos internos, você pode ler os livros de Kalen Delaney. No entanto, os livros podem ficar fora de sincronia, como em qualquer outra documentação. Além disso, há muitas combinações a serem consideradas: seis níveis de isolamento, muitos tipos de bloqueios, índices clusterizados / não clusterizados e quem sabe o que mais. Isso é muitas combinações. Além disso, o SQL Server é de código fechado, portanto, não podemos baixar o código-fonte, depurá-lo e assim por diante - isso seria a melhor fonte de conhecimento. Qualquer outra coisa pode estar incompleta ou desatualizada após o próximo lançamento ou service pack.

Portanto, você não deve decidir o que funciona para o seu sistema sem o seu próprio teste de estresse. Tudo o que você leu, pode ajudá-lo a entender o que está acontecendo, mas você deve provar que os conselhos que leu funcionam para você. Eu não acho que alguém possa fazer isso por você.

AK
fonte
9

Nesse caso em particular, a adição de um UPDLOCKbloqueio ao sistema SELECTimpediria de fato anomalias. A adição de HOLDLOCKnão é necessária, pois um bloqueio de atualização é mantido durante a transação, mas confesso que o incluí como um hábito (possivelmente ruim) no passado.

Imagine fazer algo mais legítimo do que uma atualização de ID, algum cálculo baseado em uma leitura que você precisa atualizar. Pode haver muitas tabelas envolvidas, algumas das quais você escreverá e outras que não. Qual é a melhor prática aqui?

Não há boas práticas. Sua escolha de controle de concorrência deve se basear nos requisitos do aplicativo. Alguns aplicativos / transações precisam ser executados como se tivessem propriedade exclusiva do banco de dados, evitando anomalias e imprecisões a todo custo. Outros aplicativos / transações podem tolerar algum grau de interferência um do outro.

  • Recuperando um nível de estoque em faixas (<5, 10+, 50+, 100+) para um produto em uma loja on-line = leitura suja (imprecisa não importa).
  • Verificação e redução do nível de estoque no checkout da loja on-line = leitura repetida (DEVEMOS ter o estoque antes de vender, NÃO DEVEMOS acabar com um nível de estoque negativo).
  • Mover dinheiro entre minha conta corrente e poupança no banco = serializável (não calcule mal ou extravie meu dinheiro!).

Edit: @ O comentário de AlexKuznetsov me levou a reler a pergunta e remover o erro muito óbvio em minha resposta. Nota para si mesmo em postagens noturnas.

Mark Storey-Smith
fonte