2015-06-03 9 views
27

Quindi mi sono appena reso conto che PHP è potenzialmente in grado di eseguire più richieste contemporaneamente. I log della scorsa notte sembrano mostrare che sono arrivate due richieste, sono state elaborate in parallelo; ognuno ha attivato un'importazione di dati da un altro server; ognuno ha tentato di inserire un record nel database. Una richiesta non è riuscita quando ha tentato di inserire un record che l'altro thread aveva appena inserito (i dati importati vengono forniti con PK, non sto utilizzando ID incrementali): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '865020' for key 'PRIMARY' ....Problema di concorrenza PHP, più richieste simultanee; mutex?

  1. Ho diagnosticato correttamente questo problema?
  2. Come devo affrontare questo?

Quanto segue è parte del codice. Ne ho tolto gran parte (la registrazione, la creazione di altre entità oltre il Paziente dai dati), ma i seguenti dovrebbero includere i frammenti rilevanti. Le richieste colpiscono il metodo import(), che chiama importOne() per ogni record da importare, essenzialmente. Nota il metodo di salvataggio in importOne(); questo è un metodo Eloquent (usando Laravel ed Eloquent) che genererà l'SQL per inserire/aggiornare il record come appropriato.

public function import() 
{ 
     $now = Carbon::now(); 
     // Get data from the other server in the time range from last import to current import 
     $calls = $this->getCalls($this->getLastImport(), $now); 
     // For each call to import, insert it into the DB (or update if it already exists) 
     foreach ($calls as $call) { 
      $this->importOne($call); 
     } 
     // Update the last import time to now so that the next import uses the correct range 
     $this->setLastImport($now); 
} 

private function importOne($call) 
{ 
    // Get the existing patient for the call, or create a new one 
    $patient = Patient::where('id', '=', $call['PatientID'])->first(); 
    $isNewPatient = $patient === null; 
    if ($isNewPatient) { 
     $patient = new Patient(array('id' => $call['PatientID'])); 
    } 
    // Set the fields 
    $patient->given_name = $call['PatientGivenName']; 
    $patient->family_name = $call['PatientFamilyName']; 
    // Save; will insert/update appropriately 
    $patient->save(); 
} 

Suppongo che la soluzione richiederebbe un mutex attorno all'intero blocco di importazione? E se una richiesta non potesse raggiungere un mutex, sarebbe semplicemente andare avanti con il resto della richiesta. Pensieri?

EDIT: Solo per notare, questo non è un fallimento critico. L'eccezione viene catturata e registrata e quindi la richiesta viene inviata come al solito. E l'importazione ha successo con l'altra richiesta, e quindi quella richiesta viene data come al solito. Gli utenti sono nessuno-saggi; non sanno nemmeno dell'importazione, e questo non è l'obiettivo principale della richiesta in arrivo. Quindi, davvero, potrei lasciarlo così com'è e, a parte l'eccezione occasionale, non succede niente di male. Ma se c'è una soluzione per evitare che venga fatto del lavoro aggiuntivo/richieste multiple inviate a questo altro server inutilmente, potrebbe valere la pena di proseguire.

EDIT2: Ok, ho implementato un meccanismo di blocco con flock(). Pensieri? Il seguente lavoro sarebbe? E come posso testare questa aggiunta?

public function import() 
{ 
    try { 
     $fp = fopen('/tmp/lock.txt', 'w+'); 
     if (flock($fp, LOCK_EX)) { 
      $now = Carbon::now(); 
      $calls = $this->getCalls($this->getLastImport(), $now); 
      foreach ($calls as $call) { 
       $this->importOne($call); 
      } 
      $this->setLastImport($now); 
      flock($fp, LOCK_UN); 
      // Log success. 
     } else { 
      // Could not acquire file lock. Log this. 
     } 
     fclose($fp); 
    } catch (Exception $ex) { 
     // Log failure. 
    } 
} 

Edit3: Pensieri sulla seguente implementazione alternativa del blocco:

public function import() 
{ 
    try { 
     if ($this->lock()) { 
      $now = Carbon::now(); 
      $calls = $this->getCalls($this->getLastImport(), $now); 
      foreach ($calls as $call) { 
       $this->importOne($call); 
      } 
      $this->setLastImport($now); 
      $this->unlock(); 
      // Log success 
     } else { 
      // Could not acquire DB lock. Log this. 
     } 
    } catch (Exception $ex) { 
     // Log failure 
    } 
} 

/** 
* Get a DB lock, returns true if successful. 
* 
* @return boolean 
*/ 
public function lock() 
{ 
    return DB::SELECT("SELECT GET_LOCK('lock_name', 1) AS result")[0]->result === 1; 
} 

/** 
* Release a DB lock, returns true if successful. 
* 
* @return boolean 
*/ 
public function unlock() 
{ 
    return DB::select("SELECT RELEASE_LOCK('lock_name') AS result")[0]->result === 1; 
} 
+10

Non ho nemmeno letto il contenuto della tua domanda e ti ho dato un voto. Grazie a Dio qualcuno sta facendo una vera domanda e non si limita a correggere questo errore, come arrotondare un numero, come interrogare un database! –

+0

Sì, la concorrenza è un problema. Ci sono molti modi per affrontare questo, a seconda della situazione. Blocco, blocco ottimistico, token mutex, blocchi di avviso ... Tutto dipende dalla migliore soluzione per la situazione data. Anche se sono felice anche per una domanda seria, non sono sicuro che sia ragionevolmente rispondibile in una risposta ... – deceze

+0

Hai provato a creare il tuo mutex/semaforo usando memcache? Ti aiuterà se solo un server sta scrivendo nel database. – lvil

risposta

0

vedo tre opzioni:
- uso mutex/semaforo/qualche altra bandiera - non facile da codice e mantenere
- utilizzare il meccanismo di transazione integrato DB
- utilizzare la coda (come RabbitMQ o 0MQ) per scrivere messaggi in DB di fila

+0

Penso che preferirei evitare solo il controllo della concorrenza a livello di DB, perché è troppo tardi; a quel punto, ho già colpito l'altro server per ottenere i dati da importare due volte. Preferisco che il controllo sia avvenuto abbastanza presto da impedire alla seconda richiesta di eseguire qualsiasi lavoro nel metodo import(). Quindi sembra che solo la prima opzione soddisfi questo, giusto? – Luke

+0

Come vedo l'MQ è una buona opzione: metti le strutture di controllo in un posto e lascia decidere cosa deve essere inserito. – He11ion

+0

Al momento, un framework standard chiamato Eloquent si occupa dell'interazione del database. Forse non capisco cosa stai suggerendo, ma sembra che sarebbe difficile fare una coda in quella struttura? E sento ancora che il controllo dovrebbe accadere prima, in modo tale che la seconda richiesta all'altro server non avvenga. – Luke

5

Non sembra come o hai una condizione di competizione, perché l'ID proviene dal file di importazione e, se il tuo algoritmo di importazione funziona correttamente, ogni thread avrà il suo frammento del lavoro da svolgere e non dovrebbe mai entrare in conflitto con gli altri. Ora sembra che 2 thread ricevano una richiesta per creare lo stesso paziente e entrare in conflitto tra loro a causa dell'algoritmo sbagliato.

conflictfree

assicurarsi che ogni thread spawn ottiene una nuova riga dal file di importazione, e solo ripetere in caso di fallimento.

Se non si riesce a farlo e si desidera attenersi al mutex, l'utilizzo di un blocco file non sembra una soluzione molto utile, poiché ora è stato risolto il conflitto all'interno dell'applicazione, mentre è effettivamente presente nel database. Un blocco DB dovrebbe essere molto più veloce, e nel complesso una soluzione più decente.

Richiedere un blocco del database, in questo modo:

$ db -> exec ('LOCK TABLES table1 scrivere, table2 WRITE');

E ci si può aspettare un errore SQL quando si scrive su una tabella bloccata, quindi circondare Patient-> save() con un try catch.

Una soluzione ancora migliore sarebbe utilizzare una query atomica condizionale. Una query DB che ha anche la condizione al suo interno. È possibile utilizzare una query come questa:

INSERT INTO targetTable(field1) 
SELECT field1 
FROM myTable 
WHERE NOT(field1 IN (SELECT field1 FROM targetTable)) 
+0

Non sono desiderabili più importazioni simultanee, ne voglio solo una, quindi non è necessario implementare qualcosa per condividere il lavoro tra i thread. Cosa ti fa dire che il conflitto si sta verificando nel database? Sono d'accordo che il livello DB è da dove proviene l'eccezione, ma questo perché due thread stanno facendo qualcosa che non dovrebbero fare a livello dell'applicazione. È possibile utilizzare i blocchi DB per impedirne l'importazione? Penso che mi piacerebbe che non bloccassero completamente le scritture, dal momento che altre potrebbero leggere/salvare con l'uso normale (non importare). – Luke

+0

Ah, pensavo che fossero fili separati, il mio male. Si desidera avere un blocco di scrittura, perché l'applicazione sta leggendo lo stato, quindi gestendo in base allo stato che ha assunto in precedenza. Durante il periodo tra quando si legge lo stato e si scrive l'aggiornamento in base allo stato, non si desidera che lo stato cambi nel frattempo. Esattamente quel caso d'uso dove altri scipts cambiano gli stessi dati allo stesso tempo farà mordere gli script a vicenda. Inoltre, si noti che un blocco di scrittura sul tavolo consente ancora letture, solo non aggiornamenti/inserti. – RoyB

+1

Ti suggerisco di leggere sul blocco di DB: blocco sia ottimistico che pessimistico (blocco di lettura e scrittura AKA), questo post fornisce un bel suggerimento: http://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking Non è semplice, ma credo che sia obbligatorio per ogni programmatore essere consapevole di poter scrivere software resiliente. – RoyB

4

Il codice di esempio bloccherebbe la seconda richiesta fino al termine del primo. È necessario utilizzare l'opzione LOCK_NB per flock() per restituire immediatamente l'errore e non attendere.

Sì, è possibile utilizzare il blocco o i semafori, a livello di file system o direttamente nel database.

Nel caso in cui sia necessario elaborare ciascun file di importazione solo una volta, la soluzione migliore sarebbe disporre di una tabella SQL con riga per ogni file di importazione. All'inizio dell'importazione, si inseriscono le informazioni che l'importazione è in corso, quindi altri thread sapranno di non elaborarlo nuovamente. Al termine dell'importazione, lo contrassegni come tale. (Quindi qualche ora dopo puoi controllare la tabella per vedere se l'importazione è davvero finita.)

Inoltre, è meglio fare cose di una volta di lunga durata come l'importazione su script separati e non mentre si servono le normali pagine web ai visitatori . Ad esempio, è possibile pianificare un processo cron notturno che preleva il file di importazione e lo elabora.

+0

L'importazione è un tentativo di mantenere la sincronizzazione con un altro DB, deve essere vicino al tempo reale; quindi, non di notte e ad ogni richiesta:/Grazie, guarderò a LOCK_NB! – Luke

+0

In realtà, mi hai spinto a controllare il blocco nel database. Sembra possibile utilizzare get_lock() e release_lock()? Inserirò la mia implementazione in un po '... – Luke

+0

Per la sincronizzazione quasi in tempo reale (MySQL) suggerisco uno strumento gratuito dal toolset percona chiamato 'pt-table-sync' https://www.percona.com/doc/percona- toolkit/2.2/pt-table-sync.html. Lo uso da cron ogni 5 minuti (può essere eseguito anche ogni 1 min) – Marki555