tag name php concurrency eloquent mutex

php - get tag name wordpress



Problema de concurrencia de PHP, múltiples solicitudes simultáneas; mutexes? (3)

Así que me acabo de dar cuenta de que PHP está ejecutando varias solicitudes simultáneamente. Los registros de la noche anterior parecen mostrar que se recibieron dos solicitudes, se procesaron en paralelo; cada uno activó una importación de datos de otro servidor; cada uno intentó insertar un registro en la base de datos. Una solicitud falló cuando intentó insertar un registro que el otro hilo acababa de insertar (los datos importados vienen con PKs; no estoy usando ID incrementales): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ''865020'' for key ''PRIMARY'' ...

  1. ¿He diagnosticado este problema correctamente?
  2. ¿Cómo debo abordar esto?

El siguiente es parte del código. He eliminado gran parte de ella (el registro, la creación de otras entidades más allá del Paciente de los datos), pero lo siguiente debe incluir los fragmentos relevantes. Las solicitudes presionan el método import (), que llama a importOne () para cada registro a importar, esencialmente. Tenga en cuenta el método de guardar en importOne (); ese es un método Eloquent (utilizando Laravel y Eloquent) que generará el SQL para insertar / actualizar el registro según corresponda.

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

¿Supongo que la solución requeriría un mutex en todo el bloque de importación? Y si una solicitud no pudiera alcanzar un mutex, simplemente continuaría con el resto de la solicitud. ¿Pensamientos?

EDIT: Sólo para tener en cuenta, esto no es un fallo crítico. La excepción se captura y se registra, y luego se responde a la solicitud como de costumbre. Y la importación se realiza correctamente en la otra solicitud, y luego se responde a esa solicitud como de costumbre. Los usuarios no son más sabios; ni siquiera saben sobre la importación, y ese no es el enfoque principal de la solicitud. Así que, realmente, podría dejar esto en funcionamiento tal como está, y aparte de la excepción ocasional, no pasa nada malo. Pero si existe una solución para evitar que se realicen trabajos adicionales o se envíen varias solicitudes innecesariamente a este otro servidor, podría valer la pena continuar.

EDIT2: está bien, he dado un giro en la implementación de un mecanismo de bloqueo con flock (). ¿Pensamientos? ¿Funcionaría lo siguiente? ¿Y cómo haría una prueba de unidad esta adición?

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: Reflexiones sobre la siguiente implementación alternativa del bloqueo:

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; }


No parece que tengas una condición de carrera, porque la ID proviene del archivo de importación, y si tu algoritmo de importación funciona correctamente, cada subproceso tendrá su propio fragmento del trabajo que se realizará y nunca debe entrar en conflicto con otros. Ahora parece que 2 hilos están recibiendo una solicitud para crear el mismo paciente y entrar en conflicto entre sí debido a un mal algoritmo.

Asegúrese de que cada subproceso generado obtenga una nueva fila del archivo de importación y repita solo en caso de error.

Si no puede hacer eso, y desea mantener la exclusión mutua, usar un bloqueo de archivo no parece ser una solución muy buena, ya que ahora resolvió el conflicto dentro de la aplicación, mientras que en realidad está ocurriendo en su base de datos. Un bloqueo de DB debería ser mucho más rápido también, y en general una solución más decente.

Solicitar un bloqueo de base de datos, como este:

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

Y puede esperar un error de SQL cuando escribiría en una tabla bloqueada, así que rodee a su Paciente-> guardar () con un retén de prueba.

Una solución aún mejor sería utilizar una consulta atómica condicional. Una consulta de base de datos que también tiene la condición dentro de ella. Podrías usar una consulta como esta:

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


Su código de ejemplo bloquearía la segunda solicitud hasta que la primera finalice. Necesitaría usar la opción LOCK_NB para que flock() devuelva el error inmediatamente y no espere.

Sí, puede usar bloqueo o semáforos, ya sea a nivel de sistema de archivos o directamente en la base de datos.

En su caso, cuando necesita que cada archivo de importación se procese solo una vez, la mejor solución sería tener una tabla SQL con una fila para cada archivo de importación. Al comienzo de la importación, inserta la información que la importación está en curso, por lo que otros subprocesos sabrán que no deben procesarla nuevamente. Una vez finalizada la importación, se marca como tal. (Luego, unas horas más tarde, puede consultar la tabla para ver si la importación realmente terminó).

Además, es mejor hacer cosas tan duraderas como la importación en scripts separados y no al servir páginas web normales para los visitantes. Por ejemplo, puede programar un trabajo cron nocturno que recoja el archivo de importación y lo procese.


Veo tres opciones:
- use mutex / semaphore / alguna otra bandera - no es fácil de codificar y mantener
- usar el mecanismo de transacción incorporado de DB
- usa la cola (como RabbitMQ o 0MQ) para escribir mensajes en DB en una fila