2009-07-28 6 views
21

Qual è il modo comune per gestire gli aggiornamenti simultanei in un database SQL?Come gestire gli aggiornamenti simultanei nei database?

consideri uno schema SQL semplice (i vincoli e le impostazioni predefinite non mostrati ..) come

create table credits (
    int id, 
    int creds, 
    int user_id 
); 

L'intento è quello di conservare un qualche tipo di crediti per un utente, ad esempio, qualcosa come la reputazione di StackOverflow.

Come gestire gli aggiornamenti simultanei a quella tabella? Alcune mangiare:

  • update credits set creds= 150 where userid = 1;

    In questo caso l'applicazione retreived il valore corrente, calcolato il nuovo valore (150) ed eseguito un aggiornamento. Che incantesimi se qualcun altro fa lo stesso allo stesso tempo. Suppongo di aver completato il retreival del valore corrente e l'aggiornamento in una transazione lo risolverebbe, ad es. Begin; select creds from credits where userid=1; do application logic to calculate new value, update credits set credits = 160 where userid = 1; end; In questo caso è possibile verificare se il nuovo credito sarà < 0 e troncarlo su 0 se i crediti negativi non hanno senso.

  • update credits set creds = creds - 150 where userid=1;

    Questo caso non avrebbe bisogno di preoccuparsi di aggiornamenti concorrenti come il DB si occupa del problema di coerenza, ma ha il difetto che creds sarebbero felicemente diventare negativa, che potrebbe non avere senso per alcune applicazioni .

Così semplicemente, qual è il metodo accettato di affrontare il (molto semplice) problema di cui sopra, che cosa succede se il db genera un errore?

+0

Se si è preoccupati di violare i vincoli sulle colonne, definire VINCOLI nel database. – jva

risposta

25

utilizzare le transazioni:

BEGIN WORK; 
SELECT creds FROM credits WHERE userid = 1; 
-- do your work 
UPDATE credits SET creds = 150 WHERE userid = 1; 
COMMIT; 

Alcune note importanti:

  • Non tutti i tipi di database supportano transazioni. In particolare, il tipo di database predefinito di mysql, MyISAM, non lo fa. Usa InnoDB se sei su mysql.
  • Le transazioni possono interrompersi a causa di motivi che sfuggono al tuo controllo. Se ciò accade, la tua applicazione deve essere pronta a ricominciare tutto da capo INIZIA LAVORO.
  • È necessario impostare il livello di isolamento su SERIALIZABLE, altrimenti la prima selezione può leggere i dati che altre transazioni non hanno ancora eseguito il commit (le transazioni non sono come i mutex nei linguaggi di programmazione). Alcuni database generano un errore in caso di transazioni SERIALIZABLE in corso simultanee e dovrai riavviare la transazione.
  • Alcuni DBMS forniscono SELECT .. FOR UPDATE, che bloccherà le file recuperate selezionando fino alla fine della transazione.

La combinazione di transazioni con stored procedure SQL può rendere più facile l'ultima parte da gestire; l'applicazione chiamerebbe una singola stored procedure in una transazione e la richiamerà se la transazione si interrompe.

+0

Non avresti bisogno di un "select for update" in questo caso, o almeno di un livello di isolamento SERIALIZABLE? – nos

+2

@nos, dipende dal database. Un database con supporto reale delle transazioni dovrebbe fornire un'istantanea coerente solo con una transazione, anche se probabilmente non per impostazione predefinita. Nel caso di innodb, un'istantanea dello stato del database viene creata non appena si effettua una selezione su una qualsiasi tabella innodb. – bdonlan

+0

Potrebbe essere necessario impostare il livello di isolamento tramite: http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html – bdonlan

2

Per il primo scenario è possibile aggiungere un'altra condizione nella clausola where per assicurarsi che non si sovrascrivano le modifiche apportate da un utente concorrente. Per esempio.

update credits set creds= 150 where userid = 1 AND creds = 0; 
0

Se si memorizza un ultimo timestamp aggiornamento con la cronaca, quando si legge il valore, leggere il timestamp pure. Quando vai ad aggiornare il record, controlla per assicurarsi che il timestamp corrisponda. Se qualcuno è entrato dietro di te e aggiornato prima di te, i timestamp non corrispondevano.

+2

Hai problemi se si trovano entro lo stesso millisecondo (o qualsiasi risoluzione che il tuo DB ha per data e ora). Che, naturalmente, non accade mai, fino a quando un giorno improvvisamente lo fa e lascia il tuo povero utente con crediti (o soldi) persi nel nulla. – nos

1

È possibile impostare un meccanismo di accodamento in cui le aggiunte o sottrazioni da un valore di tipo di rango vengono accodate per l'elaborazione LIFO periodica di un determinato lavoro. Se sono richieste informazioni in tempo reale sul "saldo" di un rango, ciò non si adatta perché il saldo non viene calcolato finché le voci della coda in sospeso non vengono riconciliate, ma se è qualcosa che non richiede la riconciliazione immediata potrebbe servire.

Questo sembra riflettere, almeno dall'esterno, come giochi come le vecchie serie Panzer General gestiscono le mosse individuali. Il turno di un giocatore arriva e dichiarano le loro mosse. Ogni mossa a turno viene elaborata in sequenza e non ci sono conflitti perché ogni mossa ha il suo posto nella coda.

+1

È sempre consigliabile archiviare i singoli record. È inoltre utile se nel codice sono presenti errori e è necessario ricreare il saldo dai record di origine. È anche utile perché ora è disponibile un singolo writer per la tabella di riepilogo, che riduce contesa, blocco e complessità e rende l'esperienza utente più apparente. – Doug

2

Il blocco ottimistico utilizzando una nuova colonna timestamp può risolvere questo problema di concorrenza.

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date 
12

Per le tabelle InnoDB MySQL, ciò dipende molto dal livello di isolamento impostato.

Se si utilizza il livello predefinito 3 (REPEATABLE READ), sarà necessario bloccare qualsiasi riga che influisce sulle scritture successive, anche se si è in una transazione. Nel tuo esempio è necessario:

SELECT FOR UPDATE creds FROM credits WHERE userid = 1; 
-- calculate -- 
UPDATE credits SET creds = 150 WHERE userid = 1; 

Se si utilizza livello 4 (SERIALIZABLE), quindi un semplice seguito da SELEZIONA aggiornamento è sufficiente. Il livello 4 in InnoDB viene implementato bloccando la lettura di ogni riga che leggi.

SELECT creds FROM credits WHERE userid = 1; 
-- calculate -- 
UPDATE credits SET creds = 150 WHERE userid = 1; 

Tuttavia, in questo esempio specifico, dal momento che il calcolo (l'aggiunta di crediti) è abbastanza semplice da fare in SQL, un semplice:

UPDATE credits set creds = creds - 150 where userid=1; 

sarà equivalente a un SELECT PER UPDATE seguito da UPDATE .

+0

Grazie per questa chiara ripartizione. –

0

La tabella può essere modificata come di seguito, introdurre una nuova versione di campo per gestire il blocco ottimistico. Questo è più conveniente ed efficiente per ottenere migliori prestazioni piuttosto che utilizzare blocchi a livello di database creare crediti di tabella ( int id, int creds, int user_id, int versione );

selezionare creds, user_id, versione da crediti dove user_id = 1;

assumere questo ritorna creds = 100 e la versione = 1

crediti Aggiornare Imposta creds = creds * 10, version = versione + 1 dove user_id = 1 e la versione = 1;

sempre questo assicurarsi che chi sta avendo ultimo numero di versione può aggiorna solo questo record e sporchi scrive non sarà consentito

6

Avvolgendo il codice all'interno di una transazione non è sufficiente in alcuni casi, a prescindere dal livello di isolamento si definisce.

Diciamo che avere questi passaggi e 2 filetti di concorrenza:

1) open a transaction 
2) fetch the data (SELECT creds FROM credits WHERE userid = 1;) 
3) do your work (credits + amount) 
4) update the data (UPDATE credits SET creds = ? WHERE userid = 1;) 
5) commit 

E questa linea del tempo:

Time = 0; creds = 100 
Time = 1; ThreadA executes (1) and creates Txn1 
Time = 2; ThreadB executes (1) and creates Txn2 
Time = 3; ThreadA executes (2) and fetches 100 
Time = 4; ThreadB executes (2) and fetches 100 
Time = 5; ThreadA executes (3) and adds 100 + 50 
Time = 6; ThreadB executes (3) and adds 100 + 50 
Time = 7; ThreadA executes (4) and updates creds to 150 
Time = 8; ThreadB tries to executes (4) but in the best scenario the transaction 
      (depending of isolation level) won't allow it and you get an error 

La transazione si impedisce di sovrascrivere il valore creds con un valore sbagliato, ma non è abbastanza perché non voglio fallire alcun errore

Preferisco un processo più lento che non ha mai esito negativo e ho risolto il problema con un "blocco riga del database" nel momento in cui recupero i dati (passaggio 2) che impedisce ad altri thread di leggere la stessa riga finché non ho finito con esso.

Ci sono pochi modi per farlo in SQL Server e questo è uno di loro:

SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1; 

Se ricreare la linea del tempo precedente, con questo miglioramento si ottiene qualcosa di simile:

Time = 0; creds = 100 
Time = 1; ThreadA executes (1) and creates Txn1 
Time = 2; ThreadB executes (1) and creates Txn2 
Time = 3; ThreadA executes (2) with lock and fetches 100 
Time = 4; ThreadB tries executes (2) but the row is locked and 
        it's has to wait... 

Time = 5; ThreadA executes (3) and adds 100 + 50 
Time = 6; ThreadA executes (4) and updates creds to 150 
Time = 7; ThreadA executes (5) and commits the Txn1 

Time = 8; ThreadB was waiting up to this point and now is able to execute (2) 
        with lock and fetches 150 
Time = 9; ThreadB executes (3) and adds 150 + 50 
Time = 10; ThreadB executes (4) and updates creds to 200 
Time = 11; ThreadB executes (5) and commits the Txn2 
1

Ci sono un punto critico nel tuo caso quando tu diminuisci il campo di credito corrente dell'utente per un importo richiesto e se diminuisce con successo fai altre operazioni e il problema è in teoria lì possono essere molte richieste parallele per diminuire l'operazione quando per esempio l'utente ha 1 credito a saldo e con 5 richieste di addebito di credito 1 parallelo può acquistare 5 cose se la richiesta verrà inviata esattamente nello stesso momento e si finirà con -4 crediti su il saldo dell'utente

per evitare questo si dovrebbe diminuire il valore corrente crediti con importo richiesto (nel nostro esempio 1 credito) e controllare anche in cui se il valore corrente meno quantitativo richiesto è maggiore o uguale a zero:

UPDATE crediti SET creds = creds-1 DOVE creds-1> = 0 e userid = 1

Questo sarà garanzia che l'utente non potrà mai acquistare molte cose sotto alcuni crediti se lo farà dos sistema.

Dopo questa query è necessario eseguire ROW_COUNT(), che indica se la corrente di credito utente ha incontrato i criteri e riga è stata aggiornata:

UPDATE credits SET creds = creds-1 WHERE creds-1>=0 and userid = 1 
IF (ROW_COUNT()>0) THEN 
    --IF WE ARE HERE MEANS USER HAD SURELY ENOUGH CREDITS TO PURCHASE THINGS  
END IF; 

cosa simile in un PHP può essere fatto come:

mysqli_query ("UPDATE credits SET creds = creds-$amount WHERE creds-$amount>=0 and userid = $user"); 
if (mysqli_affected_rows()) 
{ 
    \\do good things here 
} 

Qui abbiamo utilizzato né SELECT ... FOR UPDATE né TRANSACTION ma se inserisci questo codice nella transazione assicurati che il livello di transazione fornisca sempre i dati più recenti dalla riga (incluse le altre transazioni già impegnate). È inoltre possibile ROLLBACK utente se ROW_COUNT() = 0

aspetto negativo di DOVE credito- $ importo> = 0 senza bloccaggio fila sono:

Dopo l'aggiornamento è sicuramente sapere una cosa che l'utente aveva abbastanza importo sul saldo a credito anche se cerca di hackerare crediti con molte richieste, ma non sai altre cose come quello che è stato il credito prima della carica (aggiornamento) e quello che è stato il credito dopo l'addebito (aggiornamento).

Attenzione:

Non utilizzare questa strategia all'interno del livello di transazione che non fornisce i dati delle righe più recenti.

Non utilizzare questa strategia se si desidera sapere quale era il valore prima e dopo l'aggiornamento.

Basta provare a fare affidamento sul fatto che il credito è stato addebitato correttamente senza andare sotto lo zero.