Condizioni di gara nel django
-
06-07-2019 - |
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?
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 / 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 amministrazionemanage.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 filtroDOVE 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)