O que devo fazer quando o bloqueio otimista não funcionar?

11

Eu tenho este seguinte cenário:

  1. Um usuário faz uma solicitação GET/projects/1 e recebe um ETag .
  2. O usuário faz uma solicitação PUT para /projects/1com o ETag da etapa 1.
  3. O usuário faz outra solicitação de PUT /projects/1com o ETag da etapa 1.

Normalmente, a segunda solicitação PUT receberia uma resposta 412, já que o ETag agora está obsoleto - a primeira solicitação PUT modificou o recurso, para que o ETag não corresponda mais.

Mas e se as duas solicitações PUT forem enviadas ao mesmo tempo (ou exatamente uma após a outra)? A primeira solicitação PUT não tem tempo para processar e atualizar o recurso antes que a PUT # 2 chegue, o que faz com que a PUT # 2 substitua a PUT # 1. O objetivo do bloqueio otimista é que isso não aconteça ...

maximedupre
fonte
3
Atomize suas operações em transações no nível de negócios, como Esben explica abaixo.
Robert Harvey
O que aconteceria se eu atomizasse minhas operações usando transações? A PUT # 2 não seria processada até a PUT # 1 ser totalmente processada?
precisa saber é o seguinte
7
Tornar-se um pessimista?
Jpmc26
bem, é para isso que serve o bloqueio.
Fattie
Correto, é claro que o item 2 não deve ser processado - eles devem ser únicos.
Fattie

Respostas:

21

O mecanismo ETag especifica apenas o protocolo de comunicação para bloqueio otimista. É responsabilidade do serviço de aplicativo implementar o mecanismo para detectar atualizações simultâneas e reforçar o bloqueio otimista.

Em um aplicativo típico que usa um banco de dados, você normalmente faria isso abrindo uma transação ao processar uma solicitação PUT. Você normalmente lê o estado existente do banco de dados dentro dessa transação (para obter um bloqueio de leitura), verifica a validade do Etag e sobrescreve os dados (de maneira a causar um conflito de gravação quando houver uma transação simultânea incompatível), depois confirme. Se você configurar a transação corretamente, uma das confirmações falhará porque ambas tentarão atualizar os mesmos dados simultaneamente. Você poderá usar essa falha de transação para retornar 412 ou tentar novamente a solicitação, se fizer sentido para o aplicativo.

Lie Ryan
fonte
A maneira como o servidor atualmente implementa o mecanismo para detectar atualizações simultâneas é comparando os hashes do recurso. O servidor também usa transações para todas as operações, mas não estou adquirindo nenhum bloqueio, o que pode causar o problema. No entanto, no seu exemplo, como pode haver um erro em uma das confirmações se as transações estiverem usando bloqueios? A segunda transação deve estar pendente ao ler o estado, até que a primeira transação seja resolvida.
precisa saber é o seguinte
1
@maximedupre: se você estiver usando uma transação, você tem algum tipo de bloqueio, embora possa ser um bloqueio implícito (os bloqueios são adquiridos automaticamente quando você lê / atualiza os campos em vez de solicitar explicitamente). O mecanismo que descrevi acima pode ser implementado usando apenas os bloqueios implícitos. Como sua outra pergunta, isso depende do banco de dados que você está usando, mas muitos bancos de dados modernos usam MVCC (controle de simultaneidade de várias versões) para permitir que vários leitores e gravadores trabalhem nos mesmos campos sem se bloquear desnecessariamente.
Lie Ryan
1
Aviso: em muitos DBMSes (PostgreSQL, Oracle, SQL Server etc.), o nível de isolamento da transação padrão é "leitura confirmada", onde sua abordagem não é suficiente para impedir a condição de corrida do OP. Em tais DMBSes, você pode corrigi-lo incluindo a cláusula de AND ETag = ...sua UPDATEinstrução WHEREe verificando a contagem de linhas atualizada posteriormente. (Ou usando um nível de isolamento transação mais rigorosa, mas eu realmente não recomendo isso.)
ruach
1
@ruakh: depende de como você escreve sua consulta, sim, o nível de isolamento padrão não fornece esse comportamento automaticamente para todas as consultas, mas muitas vezes é possível estruturar sua transação de uma maneira que seja suficiente para implementar o bloqueio otimista. Na maioria dos casos, se a consistência da transação for importante no aplicativo, eu recomendaria a leitura repetível como nível de isolamento padrão; nos bancos de dados que usam MVCC, a sobrecarga de leitura repetível é bastante mínima e simplifica significativamente o aplicativo.
Lie Ryan
1
@ruakh: a principal desvantagem da leitura repetível é que você precisará estar preparado para tentar novamente ou falhar se houver transação simultânea. Normalmente, isso é um problema, mas os aplicativos que fornecem o bloqueio otimista como uma estratégia de simultaneidade já exigirão esse tratamento, de modo que as falhas de leitura repetíveis são mapeadas naturalmente para falhas de bloqueio otimistas e isso não adiciona novas desvantagens.
Lie Ryan
13

Você deve executar o seguinte par atomicamente:

  • verificação da validade da etiqueta (ou seja, está atualizada)
  • atualização do recurso (que inclui a atualização de sua tag)

Outros estão chamando isso de transação - mas, fundamentalmente, a execução atômica dessas duas operações é o que impede que uma substitua a outra por acidente de tempo; sem isso, você tem uma condição de corrida, como observa.

Isso ainda é considerado bloqueio otimista, se você observar o cenário geral: que o recurso em si não está bloqueado pela leitura inicial (GET) de qualquer Usuário ou Usuário que esteja visualizando os dados, com a intenção de atualizar ou não.

Algum comportamento atômico é necessário, mas isso acontece dentro de uma única solicitação (a PUT), em vez de tentar manter um bloqueio em várias interações de rede; isso é bloqueio otimista: o objeto não está bloqueado pelo GET e ainda pode ser atualizado com segurança pelo PUT.

Também existem muitas maneiras de obter a execução atômica dessas duas operações - bloquear o recurso não é a única opção; por exemplo, um bloqueio leve de thread ou objeto pode ser suficiente e depende da arquitetura e do contexto de execução do aplicativo.

Erik Eidt
fonte
4
+1 por observar que é importante ser atômico. Dependendo do recurso subjacente que está sendo atualizado, isso pode ser realizado sem transações ou bloqueio. Por exemplo, comparação atômica e troca de um recurso na memória ou fonte de eventos de dados persistentes.
Aaron M. Eshbach 05/10
@ AaronM.Eshbach, concordou e obrigado por chamá-los.
Erik Eidt
1

Cabe ao desenvolvedor do aplicativo verificar o E-Tag e fornecer essa lógica. Não é mágico que o servidor da Web faça por você, porque ele só sabe calcular E-Tagcabeçalhos para conteúdo estático. Então, vamos analisar o cenário acima e detalhar como a interação deve acontecer.

GET /projects/1

O servidor recebe a solicitação, determina o E-Tag para esta versão do registro, retornando-o com o conteúdo real.

200 - OK
E-Tag: "412"
Content-Type: application/json
{modified: false}

Como o cliente agora tem o valor de E-Tag, ele pode ser incluído na PUTsolicitação:

PUT /projects/1
If-Match: "412"
Content-Type: application/json
{modified: true}

Nesse ponto, seu aplicativo deve fazer o seguinte:

  • Verifique se o E-Tag ainda está correto: "412" == "412"?
  • Nesse caso, faça a atualização e calcule um novo E-Tag

Envie a resposta com sucesso.

204 No Content
E-Tag: "543"

Se outra solicitação surgir e tentar executar uma PUTsemelhante à solicitação acima, na segunda vez que o código do servidor avaliar, você será responsável por fornecer a mensagem de erro.

  • Verifique se o E-Tag ainda está correto: "412"! = "543"

Em caso de falha, envie a resposta da falha.

412 Precondition Failed

Este é o código que você realmente precisa escrever. O E-Tag pode de fato ser qualquer texto (dentro dos limites definidos na especificação HTTP). Não precisa ser um número. Também pode ser um valor de hash.

Berin Loritsch
fonte
Esta não é uma notação HTTP padrão que você está usando aqui. No HTTP compatível com o padrão, você usa apenas ETag em um cabeçalho de resposta. Você nunca envia ETag em um cabeçalho de solicitação, mas usa o valor ETag adquirido anteriormente em um cabeçalho If-Match ou If-None-Match nos cabeçalhos de solicitação.
Lie Ryan
-2

Como complemento às outras respostas, publicarei uma das melhores citações na documentação do ZeroMQ que descreve fielmente o problema subjacente:

Para criar programas MT totalmente perfeitos (e eu quero dizer isso literalmente), não precisamos de mutexes, bloqueios ou qualquer outra forma de comunicação entre threads, exceto mensagens enviadas pelos soquetes do ZeroMQ.

Por "programas MT perfeitos", quero dizer código fácil de escrever e entender, que funciona com a mesma abordagem de design em qualquer linguagem de programação e em qualquer sistema operacional, e que se estende por qualquer número de CPUs com zero estado de espera e nenhum ponto de retornos decrescentes.

Se você passou anos aprendendo truques para fazer com que seu código MT funcionasse, muito menos rapidamente, com bloqueios, semáforos e seções críticas, você ficará enojado quando perceber que tudo foi por nada. Se há uma lição que aprendemos de mais de 30 anos de programação simultânea, é: simplesmente não compartilhe estado. É como dois bêbados tentando compartilhar uma cerveja. Não importa se eles são bons amigos. Mais cedo ou mais tarde, eles vão entrar em uma briga. E quanto mais bêbados você adiciona à mesa, mais eles brigam por causa da cerveja. A trágica maioria dos aplicativos de MT parece brigas em bares bêbados.

lurscher
fonte