Condições de corrida em Django
-
06-07-2019 - |
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?
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/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 separadomanage.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 umWHERE 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)