web-dev-qa-db-fra.com

Incrément atomique d'un compteur en django

J'essaie d'incrémenter atomiquement un simple compteur dans Django. Mon code ressemble à ceci:

from models import Counter
from Django.db import transaction

@transaction.commit_on_success
def increment_counter(name):
    counter = Counter.objects.get_or_create(name = name)[0]
    counter.count += 1
    counter.save()

Si je comprends bien Django correctement, cela devrait envelopper la fonction dans une transaction et rendre l'incrément atomique. Mais cela ne fonctionne pas et il y a une condition de concurrence critique dans la mise à jour du compteur. Comment ce code peut-il être rendu thread-safe?

50
Björn Lindqvist

Nouveau dans Django 1.1

Counter.objects.get_or_create(name = name)
Counter.objects.filter(name = name).update(count = F('count')+1)

ou en utilisant ne expression F :

counter = Counter.objects.get_or_create(name = name)
counter.count = F('count') +1
counter.save( update_fields=["count"] )

N'oubliez pas de spécifier les champs à mettre à jour, ou vous pourriez rencontrer des conditions de concurrence sur d'autres champs possibles du modèle!

Un sujet sur la condition de compétition associée à cette approche a été ajouté à la documentation officielle.

82
Oduvan

Dans Django 1.4 il y a prise en charge de SELECT ... FOR UPDATE clauses, utilisant des verrous de base de données pour s'assurer qu'aucune donnée n'est accédée simultanément par erreur.

16
Emil Stenström

Si vous n'avez pas besoin de connaître la valeur du compteur lorsque vous le définissez, la meilleure réponse est certainement votre meilleur pari:

counter = Counter.objects.get_or_create(name = name)
counter.count = F('count') + 1
counter.save()

Cela indique à votre base de données d'ajouter 1 à la valeur de count, ce qu'elle peut parfaitement faire sans bloquer d'autres opérations. L'inconvénient est que vous n'avez aucun moyen de savoir quel count vous venez de définir. Si deux threads frappent simultanément cette fonction, ils verront tous les deux la même valeur et diront tous deux à la base de données d'ajouter 1. La base de données finira par ajouter 2 comme prévu, mais vous ne saurez pas lequel est allé en premier.

Si vous vous souciez du nombre en ce moment, vous pouvez utiliser le select_for_update option référencée par Emil Stenstrom. Voici à quoi cela ressemble:

from models import Counter
from Django.db import transaction

@transaction.atomic
def increment_counter(name):
    counter = (Counter.objects
               .select_for_update()
               .get_or_create(name=name)[0]
    counter.count += 1
    counter.save()

Cela lit la valeur actuelle et verrouille les lignes correspondantes jusqu'à la fin de la transaction. Désormais, un seul travailleur peut lire à la fois. Voir les documents pour plus d'informations sur select_for_update.

10
Xephryous

Garder les choses simples et s'appuyer sur la réponse de @ Oduvan:

counter, created = Counter.objects.get_or_create(name = name, 
                                                 defaults={'count':1})
if not created:
    counter.count = F('count') +1
    counter.save()

L'avantage ici est que si l'objet a été créé dans la première instruction, vous n'avez pas à effectuer d'autres mises à jour.

7
mlissner

Django 1.7

from Django.db.models import F

counter, created = Counter.objects.get_or_create(name = name)
counter.count = F('count') +1
counter.save()
5
derevo