MySQL: transacciones frente a tablas de bloqueo

110

Estoy un poco confundido con las transacciones frente a las tablas de bloqueo para garantizar la integridad de la base de datos y asegurarme de que SELECT y UPDATE permanezcan sincronizadas y ninguna otra conexión interfiera con ella. Necesito:

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) {
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...
}

Necesito asegurarme de que ninguna otra consulta interfiera y realice lo mismo SELECT(leyendo el 'valor anterior' antes de que la conexión termine de actualizar la fila.

Sé que puedo hacerlo de forma predeterminada para LOCK TABLES tableasegurarme de que solo 1 conexión esté haciendo esto a la vez y desbloquearlo cuando termine, pero eso parece una exageración. ¿Envolver eso en una transacción haría lo mismo (asegurando que ninguna otra conexión intente el mismo proceso mientras otra todavía está procesando)? O habría una SELECT ... FOR UPDATEo SELECT ... LOCK IN SHARE MODEser mejor?

Ryan
fuente

Respuestas:

173

Bloquear tablas evita que otros usuarios de la base de datos afecten las filas / tablas que ha bloqueado. Pero los bloqueos, por sí mismos, NO garantizarán que su lógica salga en un estado consistente.

Piense en un sistema bancario. Cuando paga una factura en línea, hay al menos dos cuentas afectadas por la transacción: Su cuenta, de la cual se toma el dinero. Y la cuenta del receptor, a la que se transfiere el dinero. Y la cuenta del banco, en la que depositarán felizmente todas las tarifas de servicio cobradas en la transacción. Dado (como todos saben estos días) que los bancos son extraordinariamente estúpidos, digamos que su sistema funciona así:

$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) {
    charge_huge_overdraft_fees();
}
$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance

Ahora, sin bloqueos ni transacciones, este sistema es vulnerable a varias condiciones de carrera, la mayor de las cuales son los pagos múltiples que se realizan en su cuenta o la cuenta del receptor en paralelo. Si bien su código tiene su saldo recuperado y está haciendo el enorme_sobredraft_fees () y todo eso, es muy posible que algún otro pago ejecute el mismo tipo de código en paralelo. Recuperarán su saldo (digamos, $ 100), harán sus transacciones (sacarán los $ 20 que está pagando y los $ 30 con los que lo están jodiendo), y ahora ambas rutas de código tienen dos saldos diferentes: $ 80 y $ 70. Dependiendo de cuáles terminen en último lugar, terminará con cualquiera de esos dos saldos en su cuenta, en lugar de los $ 50 que debería haber terminado ($ 100 - $ 20 - $ 30). En este caso, "error bancario a su favor"

Ahora, digamos que usa candados. El pago de su factura ($ 20) llega primero a la tubería, por lo que gana y bloquea el registro de su cuenta. Ahora tiene uso exclusivo y puede deducir los $ 20 del saldo y escribir el nuevo saldo en paz ... y su cuenta termina con $ 80 como se esperaba. Pero ... uhoh ... Intentas actualizar la cuenta del destinatario, y está bloqueada, y bloqueada más de lo que permite el código, agota el tiempo de tu transacción ... Estamos tratando con bancos estúpidos, así que en lugar de tener el error adecuado manejo, el código simplemente extrae unexit() , y sus $ 20 se desvanecen en una nube de electrones. Ahora te quedaste con $ 20 y todavía le debes $ 20 al receptor, y tu teléfono es embargado.

Entonces ... ingrese transacciones. Comienzas una transacción, cargas en tu cuenta $ 20, intentas acreditar al receptor con $ 20 ... y algo vuelve a explotar. Pero esta vez, en lugar de exit(), el código puede hacerrollback , y puf, tus $ 20 se agregan mágicamente a tu cuenta.

Al final, se reduce a esto:

Los bloqueos evitan que nadie más interfiera con los registros de la base de datos con los que está tratando. Las transacciones evitan que los errores "posteriores" interfieran con las cosas "anteriores" que ha realizado. Ninguno de los dos por sí solo puede garantizar que las cosas salgan bien al final. Pero juntos lo hacen.

en la lección de mañana: The Joy of Deadlocks.

Marc B
fuente
4
También estoy confundido. Digamos que la cuenta del receptor tenía $ 100 para comenzar y estamos agregando el pago de $ 20 de nuestra cuenta. Mi comprensión de las transacciones es que cuando comienzan, cualquier operación dentro de la transacción ve la base de datos en el estado en el que estaba al comienzo de la transacción. es decir: hasta que lo cambiemos, la cuenta del receptor tiene $ 100. Entonces ... cuando agregamos $ 20, en realidad establecemos un saldo de $ 120. Pero, ¿qué sucede si, durante nuestra transacción, alguien agota la cuenta del receptor a $ 0? ¿Esto se previene de alguna manera? ¿Reciben mágicamente $ 120 de nuevo? ¿Es por eso que también se necesitan candados?
Russ
Sí, ahí es donde entran en juego las cerraduras. Un sistema adecuado bloquearía la escritura en el registro para que nadie más pudiera actualizar el registro mientras avanza la transacción. Un sistema paranoico pondría un bloqueo incondicional en el registro para que nadie pudiera leer tampoco el saldo "rancio".
Marc B
1
Básicamente, mire las transacciones como asegurar cosas dentro de su ruta de código. Bloquea elementos seguros a través de rutas de código "paralelas". Hasta que lleguen los puntos muertos ...
Marc B
1
@MarcB, Entonces, ¿por qué tenemos que bloquear explícitamente si el uso de transacciones solo ya garantiza que los bloqueos estén en su lugar? ¿Habrá siquiera un caso en el que debamos realizar un bloqueo explícito porque las transacciones por sí solas son insuficientes?
Pacerier
2
Esta respuesta no es correcta y puede llevar a conclusiones erróneas. Esta declaración: "Los bloqueos evitan que nadie más interfiera con los registros de la base de datos con los que está tratando. Las transacciones evitan que los errores" posteriores "interfieran con las cosas" anteriores "que ha hecho. Ninguno de los dos por sí solo puede garantizar que las cosas funcionen bien en el final. Pero juntos, lo hacen ". - te despediría, es extremadamente incorrecto y estúpido Ver artículos: en.wikipedia.org/wiki/ACID , en.wikipedia.org/wiki/Isolation_(database_systems) y dev.mysql.com/doc/refman/5.1/ es /…
Nikola Svitlica
14

Quiere un SELECT ... FOR UPDATEo SELECT ... LOCK IN SHARE MODEdentro de una transacción, como dijo, ya que normalmente los SELECT, sin importar si están en una transacción o no, no bloquearán una tabla. El que elija dependerá de si desea que otras transacciones puedan leer esa fila mientras su transacción está en curso.

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOTno hará el truco por usted, ya que otras transacciones aún pueden aparecer y modificar esa fila. Esto se menciona en la parte superior del enlace a continuación.

Si otras sesiones actualizan simultáneamente la misma tabla, [...] es posible que vea la tabla en un estado que nunca existió en la base de datos.

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

Alison R.
fuente
7

Los conceptos de transacción y los bloqueos son diferentes. Sin embargo, la transacción utilizó bloqueos para ayudarla a seguir los principios de ACID. Si desea que la tabla evite que otros lean / escriban al mismo tiempo mientras usted lee / escribe, necesita un candado para hacer esto. Si desea asegurarse de la integridad y consistencia de los datos, es mejor que utilice las transacciones. Creo que conceptos mixtos de niveles de aislamiento en transacciones con bloqueos. Busque niveles de aislamiento de transacciones, SERIALIZE debe ser el nivel que desee.

tczhaodachuan
fuente
Esta debería ser la respuesta correcta. El bloqueo sirve para evitar condiciones de carrera y las transacciones sirven para actualizar varias tablas con datos dependientes. Dos conceptos totalmente diferentes, a pesar de que las transacciones utilizan bloqueos.
Blue Water
6

Tuve un problema similar al intentar una IF NOT EXISTS ...y luego realizar unaINSERT que causó una condición de carrera cuando varios subprocesos estaban actualizando la misma tabla.

Encontré la solución al problema aquí: Cómo escribir consultas INSERT IF NOT EXISTS en SQL estándar

Me doy cuenta de que esto no responde directamente a su pregunta, pero el mismo principio de realizar una verificación e insertar como una sola declaración es muy útil; debería poder modificarlo para realizar su actualización.

Tony
fuente
2

Estás confundido con bloqueo y transacción. Son dos cosas diferentes en RMDB. El bloqueo evita operaciones simultáneas mientras que la transacción se centra en el aislamiento de datos. Consulte este gran artículo para obtener una aclaración y una solución elegante.

David
fuente
1
Los bloqueos evitan que otros interfieran con los registros con los que está trabajando describe lo que hace de manera sucinta, y las transacciones evitan que los errores posteriores (los de otros que realizan cambios en paralelo) interfieran con las cosas anteriores que ha hecho (al permitir la reversión en caso de que alguien haya hecho algo en paralelo) resume bastante bien las transacciones ... ¿qué es lo que confunde su comprensión de estos temas?
steviesama
1

Usaría un

START TRANSACTION WITH CONSISTENT SNAPSHOT;

para empezar, y un

COMMIT;

para terminar.

Todo lo que haga en el medio está aislado de los demás usuarios de su base de datos si su motor de almacenamiento admite transacciones (que es InnoDB).

Martin Schapendonk
fuente
1
Excepto que la tabla desde la que está seleccionando no se bloqueará para otras sesiones a menos que la bloquee específicamente (o hasta que suceda su ACTUALIZACIÓN), lo que significa que otras sesiones podrían aparecer y modificarla entre SELECCIONAR y ACTUALIZAR.
Alison R.
Después de leer sobre INICIAR TRANSACCIÓN CON SNAPSHOT CONSISTENTE en la documentación de MySQL, no veo dónde realmente bloquea otra conexión para que no actualice la misma fila. Tengo entendido que vería, sin embargo, que la tabla comenzara al comienzo de la transacción. Entonces, si otra transacción está en progreso, ya obtuvo una fila y está a punto de actualizarla, la segunda transacción aún verá la fila antes de que se haya actualizado. Por lo tanto, podría intentar actualizar la misma fila que la otra transacción está a punto de realizar. ¿Es eso correcto o me falta algo en el progreso?
Ryan
1
@Ryan No hace ningún bloqueo; estás en lo correcto. El bloqueo (o no) está determinado por el tipo de operaciones que realiza (SELECCIONAR / ACTUALIZAR / ELIMINAR).
Alison R.
4
Veo. Le da consistencia de lectura a su propia transacción, pero no impide que otros usuarios modifiquen una fila justo antes que usted.
Martin Schapendonk