Bloqueo de fila InnoDB: cómo implementar

13

He estado mirando alrededor, leyendo el sitio mysql y todavía no puedo ver exactamente cómo funciona.

Quiero seleccionar y bloquear el resultado para escribir, escribir el cambio y liberar el bloqueo. AudoCommit está activado.

esquema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

Seleccione un elemento con un estado de pendiente y actualícelo para que funcione. Use una escritura exclusiva para asegurarse de que el mismo artículo no se recoja dos veces.

entonces;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

obtener la identificación del resultado

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

¿Debo hacer algo para liberar el bloqueo y funciona como lo hice anteriormente?

Wizzard
fuente

Respuestas:

26

Lo que desea es SELECCIONAR ... PARA ACTUALIZAR desde el contexto de una transacción. SELECCIONAR PARA ACTUALIZAR coloca un bloqueo exclusivo en las filas seleccionadas, como si estuviera ejecutando ACTUALIZAR. También se ejecuta implícitamente en el nivel de aislamiento READ COMMITTED, independientemente de lo que se establezca explícitamente. Solo tenga en cuenta que SELECCIONAR ... PARA ACTUALIZAR es muy malo para la concurrencia y solo debe usarse cuando sea absolutamente necesario. También tiene una tendencia a multiplicarse en una base de código a medida que las personas cortan y pegan.

Aquí hay una sesión de ejemplo de la base de datos Sakila que muestra algunos de los comportamientos de las consultas FOR UPDATE.

Primero, para que seamos claros, configure el nivel de aislamiento de la transacción en REPEATABLE READ. Esto normalmente no es necesario, ya que es el nivel de aislamiento predeterminado para InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

En la otra sesión, actualice esta fila. Linda se casó y cambió su nombre:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

De vuelta en la sesión 1, porque estábamos en REPEATABLE READ, Linda sigue siendo LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

Pero ahora, queremos acceso exclusivo a esta fila, por lo que llamamos PARA ACTUALIZAR en la fila. Observe que ahora recuperamos la versión más reciente de la fila, que se actualizó en la sesión 2 fuera de esta transacción. Eso no es REPETIBLE LEÍDO, eso es LEÍDO COMPROMETIDO

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

Probemos el bloqueo establecido en la sesión1. Tenga en cuenta que session2 no puede actualizar la fila.

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Pero aún podemos seleccionarlo

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

Y aún podemos actualizar una tabla secundaria con una relación de clave externa

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

Otro efecto secundario es que aumenta en gran medida su probabilidad de causar un punto muerto.

En su caso específico, probablemente desee:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

Si la pieza "hacer otras cosas" es innecesaria y no necesita mantener información sobre la fila, entonces SELECCIONAR PARA ACTUALIZAR es innecesario y derrochador y, en su lugar, puede ejecutar una actualización:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

Espero que esto tenga sentido.

Aaron Brown
fuente
3
Gracias. No parece resolver mi problema, cuando entran dos hilos con "SELECCIONAR ID DE itemsDONDE status= 'pendiente' LÍMITE 1 PARA LA ACTUALIZACIÓN;" y ambos ven la misma fila, entonces uno bloqueará al otro. Tenía la esperanza de que de alguna manera podría pasar por alto la fila bloqueada y pasar al siguiente elemento que estaba pendiente ...
Wizzard
1
La naturaleza de las bases de datos es que devuelven datos consistentes. Si ejecuta esa consulta dos veces antes de que se haya actualizado el valor, obtendrá el mismo resultado. No hay una extensión de SQL "Consígame el primer valor que coincida con esta consulta, a menos que la fila esté bloqueada" que conozco. Esto suena sospechosamente como si estuviera implementando una cola encima de una base de datos relacional. ¿Es ese el caso?
Aaron Brown
Aaron Sí, eso es lo que estoy tratando de hacer. He visto usar algo como Gearman, pero eso fue un fracaso. ¿Tienes algo más en mente?
Wizzard
Creo que debería leer esto: engineyard.com/blog/2011/… - para las colas de mensajes, hay muchas de ellas disponibles dependiendo del idioma de elección de su cliente. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ, etc.
Aaron Brown
¿Cómo hago para que la sesión 2 bloquee la lectura hasta que se confirme la actualización en la sesión 1?
CMCDragonkai
2

Si está utilizando el motor de almacenamiento InnoDB, utiliza el bloqueo de nivel de fila. En combinación con las versiones múltiples, esto da como resultado una buena concurrencia de consultas porque una tabla determinada puede ser leída y modificada por diferentes clientes al mismo tiempo. Las propiedades de concurrencia a nivel de fila son las siguientes:

Diferentes clientes pueden leer las mismas filas simultáneamente.

Diferentes clientes pueden modificar diferentes filas simultáneamente.

Diferentes clientes no pueden modificar la misma fila al mismo tiempo. Si una transacción modifica una fila, otras transacciones no pueden modificar la misma fila hasta que se complete la primera transacción. Otras transacciones tampoco pueden leer la fila modificada, a menos que estén utilizando el nivel de aislamiento READ UNCOMMITTED. Es decir, verán la fila original sin modificar.

Básicamente, no tiene que especificar el bloqueo explícito InnoDB lo maneja por sí mismo, aunque en algunas situaciones puede que tenga que dar detalles de bloqueo explícito sobre el bloqueo explícito a continuación:

La siguiente lista describe los tipos de bloqueo disponibles y sus efectos:

LEER

Bloquea una mesa para leer. Un bloqueo READ bloquea una tabla para consultas de lectura como SELECT que recuperan datos de la tabla. No permite operaciones de escritura como INSERT, DELETE o UPDATE que modifiquen la tabla, incluso por parte del cliente que mantiene el bloqueo. Cuando una tabla está bloqueada para leer, otros clientes pueden leer de la tabla al mismo tiempo, pero ningún cliente puede escribir en ella. Un cliente que desea escribir en una tabla que está bloqueada para lectura debe esperar hasta que todos los clientes que actualmente leen hayan terminado y liberen sus bloqueos.

ESCRIBIR

Bloquea una mesa para escribir. Un bloqueo de ESCRITURA es un bloqueo exclusivo. Se puede adquirir solo cuando no se utiliza una tabla. Una vez adquirido, solo el cliente que tiene el bloqueo de escritura puede leer o escribir en la tabla. Otros clientes no pueden leer ni escribir en él. Ningún otro cliente puede bloquear la tabla para leer o escribir.

LEER LOCAL

Bloquea una tabla para leer, pero permite inserciones concurrentes. Una inserción concurrente es una excepción al principio de "lectores bloquean escritores". Se aplica solo a las tablas MyISAM. Si una tabla MyISAM no tiene agujeros en el medio como resultado de registros eliminados o actualizados, las inserciones siempre tienen lugar al final de la tabla. En ese caso, un cliente que está leyendo una tabla puede bloquearla con un bloqueo LEER LOCAL para permitir que otros clientes se inserten en la tabla mientras el cliente que tiene el bloqueo de lectura lee. Si una tabla MyISAM tiene agujeros, puede eliminarlos utilizando OPTIMIZE TABLE para desfragmentar la tabla.

Mahesh Patil
fuente
gracias por la respuesta. Como tengo esta tabla y 100 clientes que buscan elementos pendientes, recibí muchas colisiones: 2-3 clientes obtuvieron la misma fila pendiente. El bloqueo de la mesa es lento.
Wizzard
0

Otra alternativa sería agregar una columna que almacenara el tiempo del último bloqueo exitoso y luego cualquier otra cosa que quisiera bloquear la fila tendría que esperar hasta que se borrara o transcurrieran 5 minutos (o lo que sea).

Algo como...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock es un int, ya que almacena la marca de tiempo de Unix porque es más fácil (y quizás más rápido) compararlo.

// Disculpe la semántica, no he comprobado que corran de forma aguda, pero deberían estar lo suficientemente cerca si no lo hacen.

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

Luego verifique para ver cuántas filas se actualizaron, porque dos filas no pueden actualizar las filas a la vez, si actualizó la fila, obtuvo el bloqueo. Suponiendo que está usando PHP, usaría mysql_affected_rows (), si el retorno de eso fue 1, lo bloqueó con éxito.

Luego puede actualizar el último bloqueo a 0 después de haber hecho lo que necesita hacer, o ser perezoso y esperar 5 minutos cuando el próximo intento de bloqueo tenga éxito de todos modos.

EDITAR: es posible que necesite un poco de trabajo para verificar que funcione como se espera alrededor de los cambios de horario de verano ya que los relojes retrocederían una hora, tal vez lo que anule el cheque. Debería asegurarse de que las marcas de tiempo de Unix estuvieran en UTC, lo que puede ser de todos modos.

Steve Childs
fuente
-1

Alternativamente, puede fragmentar los campos de registro para permitir la escritura paralela y omitir el bloqueo de fila (estilo de pares json fragmentados). Entonces, si un campo de un registro de lectura compuesto era un entero / real, podría tener el fragmento 1-8 de ese campo (8 registros / filas de escritura vigentes). Luego, sume los fragmentos round-robin después de cada escritura en una búsqueda de lectura separada. Esto permite hasta 8 usuarios simultáneos en paralelo.

Como solo está trabajando con cada fragmento creando un total parcial, no hay colisión y actualizaciones paralelas verdaderas (es decir, escribe bloquear cada fragmento en lugar de todo el registro de lectura unificado). Obviamente, esto solo funciona en campos numéricos. Algo que se basa en la modificación matemática para almacenar un resultado.

Por lo tanto, múltiples fragmentos de escritura por campo de lectura unificado por registro de lectura unificada. Estos fragmentos numéricos también se prestan a ECC, cifrado y transferencia / almacenamiento a nivel de bloque. Cuantos más fragmentos de escritura haya, mayores serán las velocidades de acceso de escritura paralela / concurrente en datos saturados.

MMORPG sufre enormemente con este problema, cuando un gran número de jugadores comienzan a golpearse entre sí con habilidades de Área de efecto. Esos múltiples jugadores deben escribir / actualizar a todos los demás jugadores exactamente al mismo tiempo, en paralelo, creando una tormenta de bloqueo de filas de escritura en los registros de jugadores unificados.

Mick Saunders
fuente