sql >> Base de Datos >  >> RDS >> Mysql

Consulta de actualización de MySQL:¿se respetará la condición 'dónde' en la condición de carrera y el bloqueo de fila? (php, PDO, MySQL, InnoDB)

La condición de dónde se respetará durante una situación de carrera, pero debes tener cuidado al verificar quién ganó la carrera.

Considere la siguiente demostración de cómo funciona esto y por qué debe tener cuidado.

Primero, configure algunas tablas mínimas.

CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id desempeña el papel de id en su tabla, updated_by_connection_id actúa como assignedPhone y locked como reservationCompleted .

Ahora comencemos la prueba de carrera. Debe tener 2 ventanas de línea de comando/terminal abiertas, conectadas a mysql y usando la base de datos donde ha creado estas tablas.

Conexión 1

start transaction;

Conexión 2

start transaction;

Conexión 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Conexión 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

La conexión 2 ahora está esperando

Conexión 1

SELECT * FROM table1 WHERE id = 1;
commit;

En este punto, la conexión 2 se libera para continuar y genera lo siguiente:

Conexión 2

SELECT * FROM table1 WHERE id = 1;
commit;

Todo se ve bien. Vemos que sí, se respetó la cláusula WHERE en una situación de carrera.

Sin embargo, la razón por la que dije que tenías que tener cuidado es porque en una aplicación real las cosas no siempre son tan simples. PUEDE tener otras acciones dentro de la transacción, y eso puede cambiar los resultados.

Restablezcamos la base de datos con lo siguiente:

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Y ahora, considere esta situación, donde se realiza una SELECCIÓN antes de la ACTUALIZACIÓN.

Conexión 1

start transaction;

SELECT * FROM table2;

Conexión 2

start transaction;

SELECT * FROM table2;

Conexión 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Conexión 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

La conexión 2 ahora está esperando

Conexión 1

SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

En este punto, la conexión 2 se libera para continuar y genera lo siguiente:

Bien, veamos quién ganó:

Conexión 2

SELECT * FROM table1 WHERE id = 1;

¿Esperar lo? ¿Por qué está locked? 0 y updated_by_connection_id ¿NULO?

Este es el ser cuidadoso que mencioné. El culpable se debe en realidad al hecho de que hicimos una selección al principio. Para obtener el resultado correcto, podríamos ejecutar lo siguiente:

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Al usar SELECCIONAR ... PARA ACTUALIZAR podemos obtener el resultado correcto. Esto puede ser muy confuso (como lo fue para mí, originalmente), ya que SELECCIONAR y SELECCIONAR ... PARA ACTUALIZAR están dando dos resultados diferentes.

La razón por la que esto sucede es por el nivel de aislamiento predeterminado READ-REPEATABLE . Cuando se realiza el primer SELECT, justo después de start transaction; , se crea una instantánea. Todas las lecturas futuras que no sean de actualización se realizarán a partir de esa instantánea.

Por lo tanto, si simplemente SELECCIONA ingenuamente después de realizar la actualización, extraerá la información de esa instantánea original, que es antes la fila ha sido actualizada. Al hacer SELECCIONAR... PARA ACTUALIZAR lo fuerza a obtener la información correcta.

Sin embargo, nuevamente, en una aplicación real esto podría ser un problema. Digamos, por ejemplo, que su solicitud está envuelta en una transacción y, después de realizar la actualización, desea generar cierta información. La recopilación y la salida de esa información pueden manejarse mediante un código reutilizable separado, que NO desea ensuciar con cláusulas PARA ACTUALIZAR "por si acaso". Eso generaría mucha frustración debido al bloqueo innecesario.

En cambio, querrás tomar una pista diferente. Tienes muchas opciones aquí.

Uno, es asegurarse de confirmar la transacción después de que se haya completado la ACTUALIZACIÓN. En la mayoría de los casos, esta es probablemente la mejor opción y la más sencilla.

Otra opción es no intentar usar SELECT para determinar el resultado. En su lugar, puede leer las filas afectadas y usar eso (1 fila actualizada frente a 0 filas actualizadas) para determinar si la ACTUALIZACIÓN fue un éxito.

Otra opción, y que uso con frecuencia, ya que me gusta mantener una sola solicitud (como una solicitud HTTP) completamente envuelta en una sola transacción, es asegurarme de que la primera declaración ejecutada en una transacción sea la ACTUALIZACIÓN o SELECCIONE... PARA ACTUALIZAR . Eso hará que la instantánea NO se tome hasta que se permita continuar con la conexión.

Restablezcamos nuestra base de datos de prueba nuevamente y veamos cómo funciona.

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Conexión 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Conexión 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

La conexión 2 ahora está esperando.

Conexión 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

La conexión 2 ahora está liberada.

Conexión 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Aquí podría hacer que su código del lado del servidor verifique los resultados de este SELECT y sepa que es preciso, y ni siquiera continúe con los siguientes pasos. Pero, para completar, terminaré como antes.

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Ahora puede ver que en la Conexión 2, SELECCIONAR y SELECCIONAR ... PARA ACTUALIZAR dan el mismo resultado. Esto se debe a que la instantánea de la que lee SELECT no se creó hasta después de que se confirmara la conexión 1.

Entonces, volviendo a su pregunta original:Sí, la cláusula WHERE se verifica mediante la instrucción UPDATE, en todos los casos. Sin embargo, debe tener cuidado con cualquier SELECCIÓN que pueda estar haciendo para evitar determinar incorrectamente el resultado de esa ACTUALIZACIÓN.

(Sí, otra opción es cambiar el nivel de aislamiento de la transacción. Sin embargo, realmente no tengo experiencia con eso y con cualquier problema que pueda existir, así que no voy a entrar en eso).