Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

785
Vistas
Transacción frente a consulta por lotes para evitar inserciones duplicadas de MySQL

Tengo un script PHP ( deleteAndReInsert.php ) que elimina todas las filas donde name = 'Bob' , y luego inserta 1000 filas nuevas con name = 'Bob' . Esto funciona correctamente y la tabla inicialmente vacía termina con un total de 1000 filas como se esperaba.

 $query = $pdo->prepare("DELETE FROM table WHERE name=?"); $query->execute(['Bob']); $query = $pdo->prepare("INSERT INTO table (name, age) VALUES (?,?)"); for ($i = 0; $i < 1000; $i++) { $query->execute([ 'name' => 'Bob', 'age' => 34 ]); }

El problema es que si ejecuto deleteAndReInsert.php dos veces (casi al mismo tiempo), la tabla final contiene más de 1000 filas.

Lo que parece estar sucediendo es que finaliza la consulta DELETE de la primera ejecución, y luego se llama a muchos (pero no a todos) de los 1000 INSERTS .

Luego, la segunda consulta DELETE comienza y finaliza antes de que finalicen los primeros 1000 INSERTS (digamos 350 de los 1000 INSERTS completos). Ahora se ejecutan los segundos 1000 INSERTS , y terminamos con 1650 filas en total en lugar de 1000 filas en total porque todavía quedan 1000 - 350 = 650 INSERTS después de que se llame al segundo DELETE .

¿Cuál es la forma correcta de evitar que ocurra este problema? ¿Debo envolver todo en una transacción, o debo hacer una llamada de inserción por lotes en lugar de 1000 inserciones individuales? Obviamente, puedo implementar ambas soluciones, pero tengo curiosidad por saber cuál garantiza la prevención de este problema.

over 4 years ago · Santiago Trujillo
9 Respuestas
Responde la pregunta

0

debe bloquear la operación y no liberarla antes de que finalice la inserción.

puede usar un archivo en el sistema de archivos, pero como sugirió @chris Hass, puede usar el paquete de Symfony de esta manera:

instalar el bloqueo de Symfony:

 composer require symfony/lock

deberías incluir la carga automática del compositor

 require __DIR__.'/vendor/autoload.php';

luego en su deleteAndReInsert.php :

 use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\Store\SemaphoreStore; //if you are on windows or for any reason this store(FlockStore) didnt work // you can use another stores available here: https://symfony.com/doc/current/components/lock.html#available-stores $store = new FlockStore(); $factory = new LockFactory($store); $lock = $factory->createLock('bob-recreation'); $lock->acquire(true) $query = $pdo->prepare("DELETE FROM table WHERE name=?"); $query->execute(['Bob']); $query = $pdo->prepare("INSERT INTO table (name, age) VALUES (?,?)"); for ($i = 0; $i < 1000; $i++) { $query->execute([ 'name' => 'Bob', 'age' => 34 ]); } $lock->release();

Qué pasó

como mencionaste, lo que sucedió es una condición de carrera :

Si dos procesos simultáneos acceden a un recurso compartido , eso se asemeja a la sección crítica que tal vez deba protegerse con bloqueos

over 4 years ago · Santiago Trujillo Denunciar

0

Uso de transacciones + inserción por lotes

Creo que la forma correcta de resolver el problema es usando la transacción. Vamos a hacer una eliminación + una inserción por lotes, aquí está el código:

 $pdo->beginTransaction(); $query = $pdo->prepare("DELETE FROM table WHERE name=?"); $query->execute(['Bob']); $sql = "INSERT INTO table (name, age) VALUES ".implode(', ',array_fill(0,999, '(:name, :age)')); $query = $sth->prepare($sql); $query->execute(array([ ':name' => 'Bob', 'age' => 34 ])); $pdo->commit();

Usando solo una inserción por lotes (no funcionará)

¿Por qué hacer solo una inserción por lotes no resuelve el problema? Imagina el siguiente escenario:

  1. El primer script hace una eliminación y elimina las primeras 1000 filas. ==> Forma 1000 filas a 0.
  2. El segundo script intenta hacer una eliminación pero no hay filas. ==> Forma 0 filas a 0.
  3. El primer (o segundo) script crea una inserción de 1000 lotes. ==> Forma 1000 filas a 1000.
  4. La segunda (o primera) secuencia de comandos crea una segunda inserción de 1000 lotes. ==> Forma 1000 filas a 2000.

Esta es la razón por la que el proceso es asíncrono, por lo que la segunda secuencia de comandos puede leer la tabla antes de que la primera secuencia de comandos finalice la inserción.

Usar una tabla secundaria para simular un candado (No recomendado)

Y si no tenemos transacción, ¿cómo resolveríamos ese problema? Creo que es un ejercicio de intersección.

Este es un problema de concurrencia clásico donde hay dos o más procesos que modifican los mismos datos. Para solucionar ese problema, te propongo usar una segunda tabla auxiliar para simular un Lock y controlar el acceso de concurrencia a la tabla principal.

 CREATE TABLE `access_table` ( `access` TINYINT(1) NOT NULL DEFAULT 1 )

Y en el guion

 // Here we control the concurrency do{ $query = $st->prepare('UPDATE access_table SET access = 0 WHERE access = 1'); $query ->execute(); $count = $query ->rowCount(); // You should put here a random sleep }while($count === 0); //Here we know that only us we are modifying the table $query = $pdo->prepare("DELETE FROM table WHERE name=?"); $query->execute(['Bob']); $query = $pdo->prepare("INSERT INTO table (name, age) VALUES (?,?)"); for ($i = 0; $i < 1000; $i++) { $query->execute([ 'name' => 'Bob', 'age' => 34 ]); } //And finally we open the table for other process $query = $st->prepare('UPDATE access_table SET access = 1 WHERE access = 0'); $query ->execute();

Puede adaptar la tabla a su problema, por ejemplo, si los INSERTOS/ELIMINACIONES son por nombre, puede usar un varchar(XX) para el nombre.

 CREATE TABLE `access_table` ( `name` VARCHAR(50) NOT NULL, `access` TINYINT(1) NOT NULL DEFAULT 1 )

Con este escenario

  1. El primer script cambia el valor de acceso a 0.
  2. La segunda secuencia de comandos no puede cambiar el valor, por lo que permanece en el bucle
  3. El primer script hace las ELIMINACIONES/INSERCIONES
  4. Primer script cambia el estado a 1
  5. El segundo script cambia el valor de acceso a 0 y rompe el aspecto.
  6. El segundo script hace los DELETES/INSERTS
  7. Segundo script cambia el estado a 1

Esto se debe a que las actualizaciones son atómicas, lo que significa que dos procesos no pueden actualizar la misma fecha al mismo tiempo, por lo que cuando el primer script actualiza el valor, el segundo script no puede modificar, esa acción es atómica.

Espero haberte ayudado.

over 4 years ago · Santiago Trujillo Denunciar

0

El conteo es una aproximación.

SHOW TABLE STATUS (y muchas variantes de este) proporciona solo una estimación del número de filas. (Por favor, diga cómo está obteniendo el "1650".)

La forma precisa de contar es

 SELECT COUNT(*) FROM table;

Más discusión

Hay 2 formas principales de hacer "bloqueo de transacciones". Ambos protegen contra la interferencia de otras conexiones.

  • Confirmación automática:

     SET autocommit = ON; -- probably this is the default -- Now each SQL statement is a separate "transaction"
  • COMENZAR... COMPROMETER

     BEGIN; -- (this is performed in a variety of ways by the db layer) delete... insert... COMMIT; --everything above either entire happens or is entirely ROLLBACK'd

Rendimiento:

  • DELETE --> TRUNCATE
  • Inserción por lotes (1000 filas en un solo INSERT )
  • BEGIN...COMMIT
  • LOAD DATA en lugar de INSERT

Pero ninguna de las técnicas de interpretación cambiará el problema que está encontrando, excepto "casualmente".


¿Por qué 1650?

(o algún otro número) La naturaleza transaccional de InnoDB requiere que se cuelgue de copias anteriores de filas que se eliminan o insertan hasta el COMMIT (ya sea explícito o autoconfirmado). Esto abarrota la base de datos con "filas" que podrían desaparecer. Por lo tanto, cualquier intento de estimar el número exacto de filas no es práctico.

Eso lleva a usar una técnica diferente para estimar el recuento de filas. Es algo como esto: la tabla ocupa esta cantidad de disco, y tenemos una estimación de que la fila promedio tiene esta cantidad de bytes. Divídalos para obtener el recuento de filas.

Eso lleva a que su teoría sobre la eliminación no esté terminada. En lo que respecta a cualquier SQL, la eliminación ha terminado. Sin embargo, las copias guardadas temporalmente de las 1000 filas no se han eliminado completamente de la tabla. De ahí el cálculo impreciso del número de filas.


¿Cierre?

Ninguna técnica de bloqueo "arreglará" el 1650. El bloqueo es necesario si no desea que otros subprocesos inserten/eliminen filas mientras ejecuta su experimento Eliminar+Insertar. Debe usar el bloqueo para ese propósito.

Mientras tanto, debe usar COUNT(*) si desea el conteo preciso.

over 4 years ago · Santiago Trujillo Denunciar

0

¿Cuál es la forma correcta de evitar que ocurra este problema?

Esto no es un problema y es el comportamiento esperado para dos páginas que acceden a la misma tabla en una base de datos.

¿Debo envolver todo en una transacción, o debo hacer una llamada de inserción por lotes en lugar de 1000 inserciones individuales? Obviamente, puedo implementar ambas soluciones, pero tengo curiosidad por saber cuál garantiza la prevención de este problema.

No hará una diferencia ciega aparte de limitar la cantidad de inserciones a n000 por la cantidad de páginas que ejecuta.


Escenario 1: no hacer nada

Hay dos páginas que se ejecutan una tras otra o en tiempos similares. Es por eso que está viendo 1650 registros debido a transacciones implícitas dentro del método de ejecución, lo que permite que otros procesos (páginas en su caso) accedan a los datos en una tabla.

Acción Página a página b Número de filas de la tabla
1 Elimina todos los bobs 0
... Insertar una fila 1
351 Insertar una fila Elimina todos los bobs 0
352 Insertar una fila Insertar una fila 2
... Insertar una fila Insertar una fila 4
1001 Insertar una fila Insertar una fila 1298
1002 Insertar una fila 1299
... Insertar una fila ...
1352 Insertar una fila 1650

Así se insertan 1650 Bobs.


Escenario 2: usar transacciones explícitas (optimista) | Acción | Página a | Página b | Número de filas de la tabla | Transacciones | | ----- | ------ | ------ | --- | --- | | 1 | empieza | | 0 | | | 2 | Elimina todos los Bobs | empieza | 0 | (a-d0)| | 3 | Inserta 1000 filas | Elimina todos los Bobs | 0| (a-d0-i1000)(b-d1000) | | 4 | comete | Inserta 1000 filas | 1000 | (b-d1000-i1000) | | 5 | | comete | 1000 |


Escenario 3: agregar bloqueo | Acción | Página a | Página b | Número de filas de la tabla | | ----- | ------ | ------ | --- | | 1 | Bloqueo AQ | | 0 | | 2 | empieza | | 0 | | 3 | Elimina todos los Bobs | Bloqueo AQ | 0 | | 4 | Inserta 1000 filas | sin candado| 0| | 5 | comete | sin candado | 1000 |
| 6 | | Bloqueo AQ | 1000 | | 6 | | empieza | 1000 | | 6 | | Elimina todos los Bobs | 1000 (0) | | 6 | | Inserta 1000 filas | 1000 (1000) | | 6 | | desbloquear | 1000 |

over 4 years ago · Santiago Trujillo Denunciar

0

Una alternativa a las otras soluciones es crear un archivo de bloqueo real cuando se inicia el script y verificar si existe antes de ejecutarlo.

 while( file_exists("isrunning.lock") ){ sleep(1); } //create file isrunning.lock $myfile = fopen("isrunning.lock", "w"); //deleteAndinsert code //delete lock file when finished fclose($myfile); unlink("isrunning.lock");
over 4 years ago · Santiago Trujillo Denunciar

0

Puede consultar la lista de procesos en el servidor y evitar que se ejecute su secuencia de comandos, si hay otra instancia.

over 4 years ago · Santiago Trujillo Denunciar

0

presionas deleteAndReInsert.php dos veces, y cada script tiene 1001 comandos, primero es eliminar todos los name = Bob y el resto es insertar 1000 veces Bob nuevamente. así que totalmente tiene comandos 2002, y no declara algo que haga que Mysql entienda que desea ejecutarlo sincrónicamente, y sus comandos 2002 se ejecutarán simultáneamente y conducirán al resultado inesperado. (más de 1000 name= Bob ). el proceso podría describirse así:

 ->delete `name= bob` (clear count = 0) ->insert `name = bob` ->insert `name = bob` ->insert `name = bob` ->insert `name = bob` .... ->insert `name = bob` ->delete `name= bob` (the second time deleteAndReInsert.php hit deleted at 300 times insert `name = bob` of first time deleteAndReInsert.php -> clear count rows = 0) ->insert `name = bob` ->insert `name = bob` ->insert `name = bob` .... -> insert `name = bob` (now it could be more than 1000 rows)

Entonces, si quieres, el resultado es 1000 filas. debe hacer que mysql entienda que: quiero deleteAndReInsert.php que se ejecute sincrónicamente, paso a paso. y para archivar que puedes hacer una de estas soluciones:

  1. use la declaración LOCK TABLE para bloquear la tabla y UNLOCK cuando finalice, ese segundo script no puede hacer nada con la tabla a menos que se complete el primer script.
  2. envuelva todo en la transacción BEGIN COMMIT , luego mysql se ejecutará como una acción atómica. (Bien)
  3. simule LOCK por redis (Redlock), archivo .. para que su acción se ejecute sincrónicamente (Bueno)

Espero que eso pueda ayudarte a resolver el problema.

over 4 years ago · Santiago Trujillo Denunciar

0

Lo que desea hacer es emitir LOCK TABLE ... WRITE como la primera declaración de su trabajo y RELEASE TABLES como la última.

Luego, las mil filas se eliminarán, luego se insertarán, luego se eliminarán y luego se insertarán nuevamente.

Pero todo el procedimiento me huele como un problema XY. ¿Qué es lo que realmente necesitas hacer?

Porque muchas veces he necesitado hacer algo como esto que describes ("refrescar" algunos resúmenes por ejemplo), y la mejor manera de hacerlo es, en ese escenario y en mi opinión, ni BLOQUEAR ni ELIMINAR/INSERTAR, sino

 INSERT INTO table ON DUPLICATE KEY UPDATE ...

En mi caso, si solo necesito agregar o actualizar registros, eso es suficiente.

De lo contrario, generalmente agrego un campo de "tiempo" que me permite reconocer todos los registros que quedaron "fuera" del ciclo de actualización; esos, y solo esos, se eliminan al finalizar.

Por ejemplo, digamos que necesito calcular, con un cálculo PHP complejo, la exposición financiera máxima para muchos clientes y luego insertarlo en una tabla para facilitar su uso. Cada noche se actualizan los valores de todos los clientes y, al día siguiente, se utiliza la tabla "caché". Truncar la mesa y volver a insertar todo es un fastidio.

En cambio, calculo todos los valores y construyo una consulta INSERT múltiple muy grande (podría dividirla en X consultas múltiples más pequeñas si es necesario):

 SELECT barrier:=NOW(); INSERT INTO `financial_exposures` ( ..., amount, customer_id, last_update ) VALUES ( ..., 172035.12, 12345, NOW()), ( ..., 123456.78, 12346, NOW()), ... ( ..., 450111.00, 99999, NOW()) ON DUPLICATE KEY UPDATE amount=VALUES(amount), last_update=VALUES(last_update); DELETE FROM financial_exposures WHERE last_update < @barrier;

Se insertan nuevos clientes, los clientes antiguos se actualizan a menos que sus valores no cambien (en ese caso, MySQL omite la actualización, ahorrando tiempo), y en cada instante, siempre hay un registro presente: el anterior a la actualización o el posterior a la actualización. . Los clientes eliminados se eliminan como último paso.

Esto funciona mejor cuando tiene una tabla que necesita usar y actualizar con frecuencia. Puede agregar una transacción ( SET autocommit = 0 antes de INSERT , COMMIT WORK después de DELETE ) sin bloqueos para asegurarse de que todos los clientes vean la actualización completa como si hubiera ocurrido instantáneamente.

over 4 years ago · Santiago Trujillo Denunciar

0

La respuesta de @Pericodes es correcta, pero hay un error en el fragmento de código.

Puede evitar los duplicados envolviendo el código en una transacción (no se requiere la inserción por lotes para detener los duplicados).

Es mejor usar 1 inserto de lote en lugar de 1000 insertos separados aunque no sea necesario.

Puede probar ejecutando este código dos veces (casi al mismo tiempo), y la tabla termina con exactamente 1000 registros.

 <? $pdo->beginTransaction(); $query = $pdo->prepare("DELETE FROM t1 WHERE name=?"); $query->execute(['Bob']); $query = $pdo->prepare("INSERT INTO t1 (name, age) VALUES (:name,:age)"); for ($i = 0; $i < 100; $i++) { $query->execute([ 'name' => 'Bob', 'age' => 34 ]); } $pdo->commit();

Varias de las respuestas mencionan bloqueos (nivel de base de datos y nivel de código), pero no son necesarios para este problema y son excesivos.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda