Pergunta

Aqui está um exemplo simples de uma visão de Django com uma condição de corrida em potencial:

# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points

def add_points(request):
    user = request.user
    user.points += calculate_points(user)
    user.save()

A condição de corrida deve ser bastante óbvia: um usuário pode fazer essa solicitação duas vezes, e o aplicativo pode potencialmente executar user = request.user Simultaneamente, fazendo com que um dos pedidos substitua o outro.

Suponha que a função calculate_points é relativamente complicado e faz cálculos com base em todos os tipos de coisas estranhas que não podem ser colocadas em um único update e seria difícil de colocar um procedimento armazenado.

Então, aqui está minha pergunta: que tipo de mecanismos de travamento estão disponíveis para o Django, para lidar com situações semelhantes a isso?

Foi útil?

Solução

Django 1.4+ suportes SELECT_FOR_UPDATE, em versões anteriores, você pode executar consultas SQL brutas, por exemplo, select ... for update Que, dependendo do banco de dados subjacente, bloqueará a linha de qualquer atualização, você pode fazer o que quiser com essa linha até o final da transação. por exemplo

from django.db import transaction

@transaction.commit_manually()
def add_points(request):
    user = User.objects.select_for_update().get(id=request.user.id)
    # you can go back at this point if something is not right 
    if user.points > 1000:
        # too many points
        return
    user.points += calculate_points(user)
    user.save()
    transaction.commit()

Outras dicas

A partir do Django 1.1, você pode usar as expressões f () do ORM para resolver esse problema específico.

from django.db.models import F

user = request.user
user.points  = F('points') + calculate_points(user)
user.save()

Para mais detalhes, consulte a documentação:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes baseado em-1-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.f

O bloqueio do banco de dados é o caminho a seguir aqui. Há planos para adicionar suporte "Selecionar para atualizar" ao Django (aqui), mas, por enquanto, o mais simples seria usar o SQL bruto para atualizar o objeto do usuário antes de começar a calcular a pontuação.


O bloqueio pessimista agora é suportado pelo Django 1.4's ORM quando o banco de dados subjacente (como o PostGres) o suporta. Veja o Notas de lançamento do Django 1.4A1.

Você tem muitas maneiras de thread esse tipo de coisa.

Uma abordagem padrão é Atualizar primeiro. Você faz uma atualização que apreende um bloqueio exclusivo na linha; Então faça seu trabalho; e finalmente cometer a mudança. Para que isso funcione, você precisa ignorar o cache do ORM.

Outra abordagem padrão é ter um servidor de aplicativos separado e de thread único que isola as transações da Web do cálculo complexo.

  • Seu aplicativo da Web pode criar uma fila de solicitações de pontuação, gerar um processo separado e, em seguida, escrever as solicitações de pontuação para esta fila. A desova pode ser colocada no Django's urls.py Então, isso acontece na startup da web-aplicativo. Ou pode ser colocado em separado manage.py script de administrador. Ou isso pode ser feito "conforme necessário" quando a primeira solicitação de pontuação for tentada.

  • Você também pode criar um servidor web com sabor WSGI separado usando o Werkzeug, que aceita solicitações WS via URLLIB2. Se você possui um único número de porta para este servidor, as solicitações serão filmadas pelo TCP/IP. Se o seu manipulador do WSGI tiver um thread, você alcançou um thread único serializado. Isso é um pouco mais escalável, pois o motor de pontuação é uma solicitação WS e pode ser executado em qualquer lugar.

Outra abordagem é ter outro recurso que deve ser adquirido e mantido para fazer o cálculo.

  • Um objeto Singleton no banco de dados. Uma única linha em uma tabela exclusiva pode ser atualizada com um ID da sessão para apreender o controle; Atualizar com o ID da sessão de None para liberar controle. A atualização essencial deve incluir um WHERE SESSION_ID IS NONE Filtre para garantir que a atualização falhe quando o bloqueio é mantido por outra pessoa. Isso é interessante porque é inerentemente livre de corrida-é uma única atualização-não uma sequência de atualização selecionada.

  • Um semáforo da variedade de jardim pode ser usado fora do banco de dados. As filas (geralmente) são mais fáceis de trabalhar do que com um semáforo de baixo nível.

Isso pode estar simplificando demais sua situação, mas e apenas uma substituição de link JavaScript? Em outras palavras, quando o usuário clica no link ou botão, envolva a solicitação em uma função JavaScript que desativa imediatamente / "cinza" o link e substitui o texto por "carregamento ..." ou "Solicitação de envio ..." Informações ou algo assim semelhante. Isso funcionaria para você?

Agora, você deve usar:

Model.objects.select_for_update().get(foo=bar)
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top