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?
- Ho diagnosticato correttamente questo problema?
- 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;
}
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! –
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
Hai provato a creare il tuo mutex/semaforo usando memcache? Ti aiuterà se solo un server sta scrivendo nel database. – lvil