Question

J'essaie d'implémenter (ce que je pense être) un modèle de données assez simple pour un compteur:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()

Lorsque quelqu'un arrive, une ligne correspondant au visitType et à visitDate est recherchée. si cette ligne n'existe pas, elle sera créée avec counter = 0.

Ensuite, nous incrémentons le compteur et sauvegardons.

Ce qui me préoccupe, c'est que ce processus est totalement une course. Deux demandes peuvent simultanément vérifier si l'entité est présente et toutes deux peuvent la créer. Entre la lecture du compteur et la sauvegarde du résultat, une autre requête peut arriver et l’incrémenter (ce qui entraîne une perte de compte).

Jusqu'à présent, je n'ai pas vraiment trouvé le moyen de contourner ce problème, ni dans la documentation de Django ni dans le didacticiel (en fait, il semble que le didacticiel présente une condition de concurrence critique dans la partie Vote de celui-ci).

Comment puis-je le faire en toute sécurité?

Était-ce utile?

La solution

C'est un peu un bidouillage. Le SQL brut rendra votre code moins portable, mais il supprimera la condition de concurrence critique lors de l'incrément de compteur. En théorie, cela devrait incrémenter le compteur chaque fois que vous effectuez une requête. Je n'ai pas testé cela, vous devez donc vous assurer que la liste est interpolée correctement dans la requête.

class VisitorDayTypeCounterManager(models.Manager):
    def get_query_set(self):
        qs = super(VisitorDayTypeCounterManager, self).get_query_set()

        from django.db import connection
        cursor = connection.cursor()

        pk_list = qs.values_list('id', flat=True)
        cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list])

        return qs

class VisitorDayTypeCounter(models.Model):
    ...

    objects = VisitorDayTypeCounterManager()

Autres conseils

À partir de Django 1.1, vous pouvez utiliser les expressions F () de l'ORM.

from django.db.models import F
product = Product.objects.get(name='Venezuelan Beaver Cheese')
product.number_sold = F('number_sold') + 1
product.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

Si vous voulez vraiment que le compteur soit précis, vous pouvez utiliser une transaction, mais la quantité de simultanéité requise fera réellement glisser votre application et votre base de données vers le bas sous une charge significative. Pensez plutôt à adopter une approche plus axée sur le style de messagerie et conservez simplement les enregistrements de comptage de dumping dans un tableau pour chaque visite où vous souhaitez incrémenter le compteur. Ensuite, lorsque vous souhaitez connaître le nombre total de visites, comptez le tableau des visites. Vous pouvez également avoir un processus d'arrière-plan qui s'exécute autant de fois par jour que le total des visites, puis le stocke dans la table parente. Pour économiser de l'espace, il supprimerait également tous les enregistrements de la table de visites enfants qu'il a récapitulés. Vous réduirez considérablement vos coûts d'accès simultanés si plusieurs agents ne se disputent pas les mêmes ressources (le compteur).

Vous pouvez utiliser un correctif à partir de http://code.djangoproject.com/ticket/2705 pour le verrouillage du niveau de la base de données de support.

Avec patch, ce code sera atomique:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()

Deux suggestions:

Ajoutez un unique_together à votre modèle et encapsulez la création dans un gestionnaire d'exceptions pour capturer les doublons:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()
    class Meta:
        unique_together = (('visitType', 'visitDate'))

Après cela, vous pouvez toujours avoir une condition de concurrence mineure sur la mise à jour du compteur. Si vous avez suffisamment de trafic pour vous en préoccuper, je vous conseillerais de rechercher dans les transactions un contrôle plus fin de la base de données. Je ne pense pas que l'ORM supporte directement le verrouillage / la synchronisation. La documentation de la transaction est disponible ici .

Pourquoi ne pas utiliser la base de données comme couche d'accès simultané? Ajoutez une clé primaire ou une contrainte unique de la table à visitType et visitDate. Si je ne me trompe pas, Django ne prend pas cela en charge dans leur classe Model, ou du moins je n’ai pas vu d’exemple.

Une fois que vous avez ajouté la contrainte / clé à la table, tout ce que vous avez à faire est:

  1. vérifiez si la ligne est là. si c'est le cas, allez le chercher.
  2. insérez la ligne. s'il n'y a pas d'erreur, tout va bien et vous pouvez continuer.
  3. s'il y a une erreur (c'est-à-dire une situation de concurrence critique), ré-extrayez la ligne. s'il n'y a pas de rangée, c'est une véritable erreur. Sinon, ça va.

C'est méchant de le faire de cette façon, mais cela semble assez rapide et couvrirait la plupart des situations.

Vous devez utiliser des transactions de base de données pour éviter ce type de situation de concurrence critique. Une transaction vous permet d’effectuer toute l’opération de création, de lecture, d’incrémentation et de sauvegarde du compteur sur un "tout ou rien". base. Si quelque chose ne va pas, tout sera annulé et vous pourrez réessayer.

Découvrez les documents de Django . Il existe un middleware de transaction ou vous pouvez utiliser des décorateurs autour de vues ou de méthodes pour créer des transactions.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top