2009-06-23 16 views
29

Ecco un semplice esempio di una vista django con un potenziale condizione di corsa:condizioni corsa in django

# 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 condizione di competizione dovrebbe essere abbastanza ovvio: Un utente può fare questa richiesta due volte, e l'applicazione potrebbe potenzialmente eseguire user = request.user contemporaneamente, causando la sovrascrittura di una delle richieste dell'altra.

Supponiamo che la funzione calculate_points sia relativamente complicata e che esegua calcoli basati su tutti i tipi di elementi strani che non possono essere inseriti in un singolo update e che sarebbe difficile inserire una stored procedure.

Quindi, ecco la mia domanda: che tipo di meccanismi di blocco sono disponibili per django, per gestire situazioni simili a questo?

+0

Al primo passaggio, sembra che tu abbia bisogno blocco a livello di database sulla riga in questione in quel punto. Vorrei consultare la documentazione SQL per il tuo database e inviare una query personalizzata per farlo. –

+1

Preferirei una soluzione "database-agnostica" se è possibile. – Fragsworth

+1

'@ transaction.commit_on_success' +' QuerySet.select_for_update() ' – orokusaki

risposta

38

Django 1.4+ supporta select_for_update, nelle versioni precedenti è possibile eseguire query SQL prime per esempio select ... for update che, a seconda del DB sottostante, bloccherà la riga da eventuali aggiornamenti, è possibile fare tutto ciò che si desidera con quella riga fino alla fine della transazione. per esempio.

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

Questa dovrebbe essere la risposta accettata. –

+0

Sembra che ci fosse una patch per molto tempo per questa funzione https://code.djangoproject.com/ticket/2705 - L'ho recentemente applicata a Django 1.3.5 (per un progetto di grandi dimensioni, difficile da migrare a 1.4) – HighCat

+0

Mi chiedo come sia meglio implementarlo come metodo della classe User (da riusare in altri posti, non solo in quella vista).Il problema per me è che il codice chiamante deve ancora effettuare la chiamata select_for_update(), ma mi piacerebbe che fosse incapsulato nel metodo dell'utente. –

6

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

Un approccio standard è Aggiornamento prima. Fai un aggiornamento che bloccherà un blocco esclusivo sulla riga; quindi fai il tuo lavoro; e finalmente commetti il ​​cambiamento. Affinché funzioni, è necessario bypassare la memorizzazione nella cache dell'ORM.

Un altro approccio standard consiste nell'avere un server applicazioni separato a thread singolo che isola le transazioni Web dal calcolo complesso.

  • L'applicazione web può creare una coda di richieste di punteggio, generare un processo separato, e poi scrivere le richieste di punteggio per questa coda. Lo spawn può essere inserito in Django urls.py in modo che accada all'avvio della web-app. Oppure può essere inserito in uno script di amministrazione separato manage.py. Oppure può essere fatto "secondo necessità" quando viene tentata la prima richiesta di punteggio.

  • È inoltre possibile creare un server Web separato con funzionalità WSGI utilizzando Werkzeug che accetta richieste WS tramite urllib2. Se si dispone di un numero di porta singolo per questo server, le richieste vengono accodate da TCP/IP. Se il tuo gestore WSGI ha un thread, allora hai raggiunto il single-threading serializzato. Questo è leggermente più scalabile, poiché il motore di calcolo del punteggio è una richiesta WS e può essere eseguito ovunque.

Un altro approccio è avere un'altra risorsa che deve essere acquisita e tenuta per eseguire il calcolo.

  • Un oggetto Singleton nel database. Una singola riga in una tabella univoca può essere aggiornata con un ID di sessione per acquisire il controllo; aggiornare con l'ID di sessione di None per rilasciare il controllo. L'aggiornamento essenziale deve includere un filtro WHERE SESSION_ID IS NONE per garantire che l'aggiornamento non riesca quando il blocco è trattenuto da qualcun altro. Questo è interessante perché è intrinsecamente privo di corsa - si tratta di un singolo aggiornamento - non di una sequenza SELECT-UPDATE.

  • Un semaforo da giardino può essere utilizzato all'esterno del database. Le code (generalmente) sono più facili da utilizzare rispetto a un semaforo di basso livello.

+1

+1 Mi piace molto l'idea della coda di richieste di punteggio. –

+0

Ottima risposta. In qualche modo l'accesso alla riga del database deve essere serializzato e penso che le code siano più scalabili dei lock. @Fragsworth: guarda questo progetto per un'implementazione semplice delle code in Django che usa RabbitMQ: http://ask.github.com/celery/introduction.html –

8

Il blocco del database è il modo per andare qui. Ci sono piani per aggiungere il supporto "seleziona per l'aggiornamento" a Django (here), ma per ora il più semplice sarebbe utilizzare SQL raw per AGGIORNARE l'oggetto utente prima di iniziare a calcolare il punteggio.


blocco pessimistico è ora supportato da ORM Django 1.4, quando il DB sottostante (come Postgres) supporta. Vedi lo Django 1.4a1 release notes.

1

Ciò può semplificare eccessivamente la situazione, ma per quanto riguarda la sostituzione di un collegamento JavaScript? In altre parole quando l'utente fa clic sul link o sul pulsante avvolge la richiesta in una funzione JavaScript che disabilita immediatamente/"cancella" il collegamento e sostituisce il testo con le informazioni "Caricamento in corso ..." o "Invio richiesta ..." o qualcosa del genere simile. questo funzionerebbe per te?

+2

-1 non protegge ancora il sito. di volta in volta gli utenti utilizzano altri client http rispetto ai browser. Ad esempio, l'utente potrebbe utilizzare wget per recuperare l'URL specificato, quindi disabilitare URL da jscript non ti salverà. Jscript dovrebbe essere usato solo per rendere friednly l'utente della pagina, se lo desideri, ma non dovresti usarlo per correggere i problemi all'interno dell'applicazione lato server. – SashaN

+0

@SashaN: il poster non ha detto che questo non sarebbe accessibile solo tramite un browser web. Non possiamo assumere immediatamente tutti gli altri casi di eccezione come wget. Ho anche prefisso la risposta con "Questo potrebbe semplificare eccessivamente la tua situazione ..." per coprire i casi di eccezione, in quanto questo suggerimento potrebbe essere una soluzione adatta per molti. Pensa anche ai futuri spettatori di questa domanda che potrebbero avere uno scenario leggermente diverso in cui questa risposta potrebbe essere solo il ticket. Certamente non accetto il fatto che meriti un voto "non utile", ma apprezzo che almeno fornisci una motivazione. –

+1

"Non ti fidi del lato client" – Ekevoo

14

A partire da Django 1.1 è possibile utilizzare le espressioni F() dell'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 si veda la documentazione:

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

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

+4

Le espressioni 'F()' non ti permettono ancora di aggiungere un condizionale all'aggiornamento. Quindi potresti dire aumentare i punti degli utenti se sono ancora attivi. –

+0

no ... questo fallirebbe se si aggiornasse all'interno di un ciclo for! – NoobEditor

0

Ora, è necessario utilizzare:

Model.objects.select_for_update().get(foo=bar) 
+1

Una spiegazione delle tue intenzioni sarebbe migliorare la tua risposta . – reporter