Domanda

Sto cercando di implementare (quello che penso sia) un modello di dati piuttosto semplice per un contatore:

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

Quando arriva qualcuno, cercherà una riga che corrisponda a visitType e visitDate; se questa riga non esiste, verrà creata con counter = 0.

Quindi incrementiamo il contatore e salviamo.

La mia preoccupazione è che questo processo sia totalmente una gara. Due richieste potrebbero verificare contemporaneamente se l'entità è presente ed entrambe potrebbero crearla. Tra la lettura del contatore e il salvataggio del risultato, potrebbe venire un'altra richiesta e incrementarla (con conseguente perdita del conteggio).

Finora non ho davvero trovato un buon modo per aggirare questo problema, né nella documentazione di Django o nel tutorial (in effetti, sembra che il tutorial abbia una condizione di gara nella parte Vota di esso).

Come posso farlo in sicurezza?

È stato utile?

Soluzione

Questo è un po 'un trucco. Il codice SQL non elaborato renderà il tuo codice meno portatile, ma eliminerà le condizioni di competizione sull'incremento del contatore. In teoria, questo dovrebbe incrementare il contatore ogni volta che si esegue una query. Non l'ho provato, quindi dovresti assicurarti che l'elenco venga interpolato correttamente nella query.

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

Altri suggerimenti

Da Django 1.1 puoi usare le espressioni F () dell'ORM.

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

Se vuoi davvero che il contatore sia accurato, potresti usare una transazione ma la quantità di concorrenza richiesta trascinerà davvero la tua applicazione e il tuo database sotto qualsiasi carico significativo. Pensa invece di adottare un approccio più stile di messaggistica e tieni semplicemente i record di conteggio dei dumping in una tabella per ogni visita in cui desideri incrementare il contatore. Quindi, quando vuoi il numero totale di visite, conta sulla tabella delle visite. Potresti anche avere un processo in background che viene eseguito un numero qualsiasi di volte al giorno per sommare le visite e memorizzarlo nella tabella padre. Per risparmiare spazio, eliminerebbe anche tutti i record dalla tabella delle visite figlio che ha riassunto. Ridurrai notevolmente i costi della concorrenza se non hai più agenti in lizza per le stesse risorse (il contatore).

Puoi usare la patch da http://code.djangoproject.com/ticket/2705 per il blocco a livello di database di supporto.

Con patch questo codice sarà atomico:

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

Due suggerimenti:

Aggiungi un unico_insieme al tuo modello e avvolgi la creazione in un gestore di eccezioni per catturare i duplicati:

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

Dopo questo, potresti avere una condizione di gara minore sull'aggiornamento del contatore. Se si ottiene abbastanza traffico per preoccuparsi di ciò, suggerirei di esaminare le transazioni per un controllo più accurato del database. Non credo che l'ORM abbia un supporto diretto per il blocco / sincronizzazione. La documentazione della transazione è disponibile qui .

Perché non utilizzare il database come livello di concorrenza? Aggiungi una chiave primaria o un vincolo univoco alla tabella visitType e visitDate. Se non sbaglio, django non supporta esattamente questo nella loro classe Model del database o almeno non ne ho visto un esempio.

Dopo aver aggiunto il vincolo / chiave alla tabella, tutto ciò che devi fare è:

  1. controlla se la riga è presente. se lo è, recuperalo.
  2. inserisci la riga. se non c'è errore stai bene e puoi andare avanti.
  3. se si verifica un errore (ad es. condizioni di gara), recuperare nuovamente la riga. se non c'è riga, allora è un vero errore. Altrimenti, stai bene.

È brutto farlo in questo modo, ma sembra abbastanza veloce e coprirebbe la maggior parte delle situazioni.

Dovresti usare le transazioni del database per evitare questo tipo di condizioni di gara. Una transazione ti consente di eseguire l'intera operazione di creazione, lettura, incremento e salvataggio del contatore su un "tutto o niente" base. Se qualcosa va storto, tornerà indietro e puoi riprovare.

Consulta i documenti Django. Esiste un middleware di transazione oppure è possibile utilizzare decoratori attorno a viste o metodi per creare transazioni.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top