Domanda

Ecco un semplice esempio di una vista django con una potenziale condizione di gara:

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

Le condizioni di gara dovrebbero essere abbastanza ovvie: un utente può fare questa richiesta due volte e l'applicazione potrebbe potenzialmente eseguire contemporaneamente user = request.user , facendo sì che una delle richieste prevalga sull'altra.

Supponiamo che la funzione calcola_punti sia relativamente complicata e faccia calcoli basati su tutti i tipi di cose strane che non possono essere inserite in un singolo aggiornamento e che sarebbero difficili da inserire una procedura memorizzata.

Quindi, ecco la mia domanda: che tipo di meccanismi di blocco sono disponibili per Django, per affrontare situazioni simili a questa?

È stato utile?

Soluzione

Django 1.4+ supporta select_for_update , nelle versioni precedenti è possibile eseguire query SQL non elaborate, ad es seleziona ... per l'aggiornamento che, a seconda del DB sottostante, bloccherà la riga da eventuali aggiornamenti, puoi fare quello che vuoi con quella riga fino alla fine della transazione. per es.

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

Altri suggerimenti

A partire da Django 1.1 è possibile utilizzare le espressioni F () di ORM per risolvere questo problema specifico.

from django.db.models import F

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

Per maggiori dettagli consultare la documentazione:

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

https: //docs.djangoproject .com / it / 1.8 / ref / modelli / espressioni / # django.db.models.F

Il blocco del database è la strada da percorrere qui. Ci sono piani per aggiungere " selezionare per l'aggiornamento " supporto a Django ( qui ), ma per ora il più semplice sarebbe usare SQL raw per AGGIORNARE l'oggetto utente prima di iniziare a calcolare il punteggio.


Il blocco pessimistico è ora supportato dall'ORM di Django 1.4 quando il DB sottostante (come Postgres) lo supporta. Vedi le Django 1.4a1 note di rilascio .

Hai molti modi per eseguire il thread singolo di questo tipo di cose.

Un approccio standard è Aggiorna prima . Si esegue un aggiornamento che impadronirà di un blocco esclusivo sulla riga; poi fai il tuo lavoro; e infine impegnare il cambiamento. Affinché ciò funzioni, è necessario ignorare la memorizzazione nella cache dell'ORM.

Un altro approccio standard è quello di disporre di un server delle applicazioni a thread singolo separato che isola le transazioni Web dal calcolo complesso.

  • La tua applicazione web può creare una coda di richieste di punteggio, generare un processo separato e quindi scrivere le richieste di punteggio su questa coda. Lo spawn può essere inserito nel urls.py di Django in modo che avvenga all'avvio dell'app web. Oppure può essere inserito in uno script di amministrazione manage.py separato. Oppure può essere fatto " secondo necessità " quando viene tentata la prima richiesta di punteggio.

  • Puoi anche creare un web server separato basato su WSGI usando Werkzeug che accetta le richieste WS tramite urllib2. Se si dispone di un unico numero di porta per questo server, le richieste vengono accodate da TCP / IP. Se il tuo gestore WSGI ha un thread, allora hai ottenuto il threading singolo serializzato. Questo è leggermente più scalabile, poiché il motore di calcolo del punteggio è una richiesta WS e può essere eseguito ovunque.

Ancora un altro approccio è avere qualche altra risorsa che deve essere acquisita e trattenuta per fare il calcolo.

  • Un oggetto Singleton nel database. Una singola riga in una tabella univoca può essere aggiornata con un ID sessione per impadronirsi del controllo; aggiornamento con ID sessione di Nessuno per rilasciare il controllo. L'aggiornamento essenziale deve includere un filtro DOVE SESSION_ID È NESSUNO per garantire che l'aggiornamento non riesca quando il blocco viene trattenuto da qualcun altro. Questo è interessante perché intrinsecamente privo di razza - è un singolo aggiornamento - non una sequenza SELECT-UPDATE.

  • È possibile utilizzare un semaforo di varietà da giardino all'esterno del database. Le code (in genere) sono più facili da lavorare rispetto a un semaforo di basso livello.

Questo potrebbe semplificare eccessivamente la tua situazione, ma per quanto riguarda solo una sostituzione del link JavaScript? In altre parole, quando l'utente fa clic sul collegamento o sul pulsante, avvolge la richiesta in una funzione JavaScript che disabilita immediatamente / "disabilita" " il link e sostituisce il testo con " Caricamento in corso ... " o " Invio richiesta ... " informazioni o qualcosa di simile. Funzionerebbe per te?

Ora devi usare:

Model.objects.select_for_update().get(foo=bar)
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top