Question

Voici un exemple simple d'une vue Django avec une condition de concurrence potentielle:

# 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()

La condition de concurrence devrait être assez évidente: un utilisateur peut effectuer cette demande deux fois et l'application peut éventuellement exécuter user = request.user simultanément, ce qui entraîne le remplacement de l'une des demandes par l'autre.

Supposons que la fonction Calculate_points soit relativement compliquée et fasse des calculs basés sur toutes sortes de choses étranges qui ne peuvent pas être placées dans une seule mise à jour et seraient difficiles à intégrer. une procédure stockée.

Voici donc ma question: quels types de mécanismes de verrouillage sont disponibles pour Django, pour faire face à des situations similaires?

Était-ce utile?

La solution

Django 1.4+ prend en charge select_for_update , dans les versions antérieures, vous pouvez exécuter des requêtes SQL brutes, par exemple sélectionnez ... pour la mise à jour qui, en fonction de la base de données sous-jacente, verrouille la ligne à partir de toute mise à jour. Vous pouvez faire ce que vous voulez avec cette ligne jusqu'à la fin de la transaction. par exemple

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()

Autres conseils

À partir de Django 1.1, vous pouvez utiliser les expressions F () de l'ORM pour résoudre ce problème spécifique.

from django.db.models import F

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

Pour plus de détails, consultez la documentation:

https: / /docs.djangoproject.com/fr/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https: //docs.djangoproject .com / fr / 1.8 / ref / models / expressions / # django.db.models.F

Le verrouillage de la base de données est le moyen d'aller ici. Il est prévu d’ajouter "Sélectionner pour la mise à jour". support à Django ( ici ), mais pour l'instant, le plus simple serait d'utiliser du SQL brut pour UPDATE l'objet utilisateur avant de commencer à calculer le score.

Le verrouillage pessimiste est désormais pris en charge par l'ORM de Django 1.4 lorsque la base de données sous-jacente (telle que Postgres) le prend en charge. Voir le Notes de publication de Django 1.4a1 .

Vous avez plusieurs façons de mettre en file indienne ce genre de chose.

Une des approches standard est la Mise à jour en premier . Vous faites une mise à jour qui saisira un verrou exclusif sur la ligne; alors faites votre travail; et enfin commettre le changement. Pour que cela fonctionne, vous devez contourner la mise en cache de l'ORM.

Une autre approche standard consiste à utiliser un serveur d’application séparé, à un seul thread, qui isole les transactions Web du calcul complexe.

  • Votre application Web peut créer une file d'attente de demandes d'évaluation, générer un processus séparé, puis écrire les demandes d'évaluation dans cette file d'attente. Le spawn peut être placé dans le urls.py de Django afin qu'il se produise au démarrage de l'application Web. Vous pouvez également le placer dans un script administrateur distinct manage.py . Ou cela peut être fait "au besoin" lors de la première tentative d'évaluation.

  • Vous pouvez également créer un serveur Web distinct au goût WSGI à l'aide de Werkzeug qui accepte les demandes WS via urllib2. Si vous avez un seul numéro de port pour ce serveur, les demandes sont mises en file d'attente par TCP / IP. Si votre gestionnaire WSGI a un seul thread, vous avez atteint un seul thread sérialisé. Ceci est légèrement plus évolutif, car le moteur de scoring est une requête WS et peut être exécuté n’importe où.

Encore une autre approche consiste à disposer d’une autre ressource à acquérir et à conserver pour effectuer le calcul.

  • Un objet Singleton dans la base de données. Une seule ligne dans une table unique peut être mise à jour avec un identifiant de session pour saisir le contrôle; mettre à jour avec l'ID de session None pour libérer le contrôle. La mise à jour essentielle doit inclure un filtre WHERE SESSION_ID IS NONE pour garantir que la mise à jour échoue lorsque le verrou est maintenu par une autre personne. C’est intéressant parce que, par nature, il n’ya pas de race - c’est une mise à jour unique - pas une séquence SELECT-UPDATE.

  • Un sémaphore de variété de jardin peut être utilisé en dehors de la base de données. Les files d'attente (en général) sont plus faciles à manipuler qu'avec un sémaphore de bas niveau.

Cela simplifie peut-être excessivement votre situation, mais qu’en est-il du remplacement d’un lien JavaScript? En d’autres termes, lorsque l’utilisateur clique sur le lien ou le bouton, la requête est encapsulée dans une fonction JavaScript qui désactive immédiatement / "grise". le lien et remplace le texte par " Chargement ... " ou "Soumission de la demande ... " info ou quelque chose de similaire. Cela fonctionnerait-il pour vous?

Maintenant, vous devez utiliser:

Model.objects.select_for_update().get(foo=bar)
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top