2010-09-29 13 views
42

Tengo una pregunta sobre SQL y estrategias de bloqueo. Como ejemplo, supongamos que tengo un contador de vistas para las imágenes en mi sitio web. Si tengo un procedimiento almacenado o similares para realizar las siguientes declaraciones:Incremento atómico SQL y estrategias de bloqueo: ¿es esto seguro?

START TRANSACTION; 
UPDATE images SET counter=counter+1 WHERE image_id=some_parameter; 
COMMIT; 

Supongamos que el contador para un image_id específica tiene un valor '0' en el tiempo t0. Si dos sesiones actualizan el mismo contador de imágenes, s1 y s2, comienzan al mismo tiempo en t0, ¿hay alguna posibilidad de que estas dos sesiones lean el valor '0', aumenten a '1' y ambas intenten actualizar el contador a '1 ', ¿entonces el contador obtendrá el valor' 1 'en lugar de' 2 '?

s1: begin 
s1: begin 
s1: read counter for image_id=15, get 0, store in temp1 
s2: read counter for image_id=15, get 0, store in temp2 
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1 
s1: commit, ok 
s2: commit, ok 

Resultado final: valor incorrecto '1' para image_id = 15, debería haber sido 2.

Mis preguntas son:

  1. ¿Es este escenario posible?
  2. Si es así, ¿importa el nivel de aislamiento de transacción?
  3. ¿Hay una resolución de conflictos que detectaría un conflicto como un error?
  4. ¿Se puede usar alguna sintaxis especial para evitar un problema (algo así como Compare And Swap (CAS) o técnicas de bloqueo explícitas)?

Me interesa una respuesta general, pero si no hay ninguna, me interesan las respuestas específicas de MySql e InnoDB, ya que intento utilizar esta técnica para implementar secuencias en InnoDB.

EDITAR: El siguiente escenario también podría ser posible, dando como resultado el mismo comportamiento. Supongo que estamos en el nivel de aislamiento READ_COMMITED o superior, por lo que s2 obtiene el valor desde el inicio de la transacción, aunque s1 ya escribió '1' en el contador.

s1: begin 
s1: begin 
s1: read counter for image_id=15, get 0, store in temp1 
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: read counter for image_id=15, get 0 (since another tx), store in temp2 
s2: write counter for image_id=15 to (temp2+1), which is also 1 
s1: commit, ok 
s2: commit, ok 
+0

mysql http://stackoverflow.com/questions/4358732/is-incrementing-a-field-in-mysql-atomic || MS http://stackoverflow.com/questions/193257/in-ms-sql-server-is-there-a-way-to-atomically-increment-a-column-being-used-a –

Respuesta

28

UPDATE consulta coloca un bloqueo de actualización en las páginas o registros que lee.

Cuando se decide si actualizar el registro, el bloqueo se levanta o se promueve al bloqueo exclusivo.

Esto significa que en este escenario:

s1: read counter for image_id=15, get 0, store in temp1 
s2: read counter for image_id=15, get 0, store in temp2 
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1 

s2 esperará hasta s1 decide si se debe escribir el mostrador o no, y este escenario es de hecho imposible.

Será esto:

s1: place an update lock on image_id = 15 
s2: try to place an update lock on image_id = 15: QUEUED 
s1: read counter for image_id=15, get 0, store in temp1 
s1: promote the update lock to the exclusive lock 
s1: write counter for image_id=15 to (temp1+1), which is 1 
s1: commit: LOCK RELEASED 
s2: place an update lock on image_id = 15 
s2: read counter for image_id=15, get 1, store in temp2 
s2: write counter for image_id=15 to (temp2+1), which is 2 

Nótese que en InnoDB, DML consultas no se levantan los bloqueos de actualización de los registros que leen.

Esto significa que en el caso de una exploración de tabla completa, los registros que se leyeron pero decidieron no actualizar, permanecerán bloqueados hasta el final de la transacción y no se pueden actualizar desde otra transacción.

+1

Gracias por la gran explicación. Creo que la frase clave aquí es 'commit: LOCK RELEASED'. Esto significa que todas las transacciones que desean actualizar la fila deben esperar la finalización de la transacción que contiene el bloqueo, serializando efectivamente todas las transacciones contendientes para la fila. ¿Sabes cómo funcionaría esto en una versión múltiple de concurrencia de db como Postgres? Dado que usa múltiples versiones, supongo que permitirá que las transacciones progresen de forma independiente e intente combinar los resultados. ¿O emplea la misma estrategia? –

+1

¿Hay un nivel mínimo de aislamiento de transacción necesario para que s1 coloque un bloqueo de actualización al inicio como lo sugiere ConcernedOfTunbridgeWells en su respuesta? –

+0

@disown: en 'MVCC' que utiliza' PostgreSQL', no existe el concepto de bloqueo. En cambio, se almacenan múltiples versiones de un registro, usando el identificador de transacción como marcador. El bloqueo solo ocurre cuando se trata de modificar una versión en el limbo. – Quassnoi

8

Si el bloqueo no se hace correctamente sin duda es posible obtener este tipo de condición de carrera, y el modo de bloqueo por defecto (lectura confirmada) no lo permite. En este modo, las lecturas solo colocan un bloqueo compartido en el registro, por lo que ambos pueden ver 0, incrementarlo y escribir 1 en la base de datos.

Para evitar esta condición de carrera, debe establecer un bloqueo exclusivo en la operación de lectura. Los modos de concurrencia 'Serializable' y 'Lectura repetible' harán esto, y para una operación en una sola fila son prácticamente equivalentes.

Para que sea completamente atómica que tiene que:

  • Establecer una adecuada transaction isolation level como Serializable. Normalmente puede hacer esto desde una biblioteca de cliente o explicilidad en SQL.
  • iniciar la transacción
  • leer los datos
  • actualizarlo
  • confirmar la transacción.

También puede forzar un bloqueo exclusivo en la lectura con un HOLDLOCK (T-SQL) o sugerencia equivalente, según su dialecto SQL.

Una única consulta de actualización lo hará atómicamente pero no se puede dividir la operación (quizás para leer el valor y devolverlo al cliente) sin asegurarse de que las lecturas eliminen un bloqueo exclusivo. Deberá obtener el valor atómico para implementar una secuencia, por lo que la actualización en sí misma probablemente no sea todo lo que necesita. Incluso con la actualización atómica, todavía tiene una condición de carrera para leer el valor después de la actualización. La lectura tendrá que llevarse a cabo dentro de una transacción (almacenando lo que obtuvo en una variable) y emitirá un bloqueo exclusivo durante la lectura.

Tenga en cuenta que para hacer esto sin crear un punto caliente su base de datos debe tener soporte adecuado para autonomous (nested) transactions dentro de un procedimiento almacenado. Tenga en cuenta que a veces 'anidado' se utiliza para referirse a transacciones de encadenamiento o puntos de guardado, por lo que el término puede ser un poco confuso. He editado esto para referirme a transacciones autónomas.

Sin transacciones autónomas, sus bloqueos son heredados por la transacción padre, que puede deshacer todo el lote. Esto significa que se mantendrán hasta que la transacción padre se comprometa, lo que puede convertir su secuencia en un punto caliente que serialice todas las transacciones usando esa secuencia. Cualquier otra cosa que intente usar la secuencia se bloqueará hasta que la transacción principal se comprometa.

IIRC Oracle admite transacciones autónomas, pero DB/2 no lo hizo hasta hace relativamente poco tiempo y SQL Server no. Fuera de mi cabeza, no sé si InnoDB los admite, pero Grey and Reuter continúa con bastante detalle sobre lo difícil que es implementarlos. En la práctica, supongo que es bastante probable que no sea así. YMMV.

+0

Con el único 'UPDATE 'consulta, no es posible obtener esta condición de carrera en ninguno de los principales sistemas que admiten transacciones, sin importar qué nivel de aislamiento de transacción se use. – Quassnoi

+0

Si entiendo el problema correctamente, tener aislamiento no es suficiente, SERIALIZABLE podría ser, pero tal vez no el aislamiento de instantáneas (http://en.wikipedia.org/wiki/Snapshot_isolation), ya que no estoy seguro acerca de la definición de ' conflicto 'en este caso. ¿Están los dos en conflicto o no? E incluso si tiene un bloqueo exclusivo en la fila durante la actualización, ¿qué pasa si la actualización de s2: s viene entre la actualización de s1: s y la confirmación?¿Qué leerá s2 entonces? Posiblemente 0. La única forma en que puedo ver para asegurar el comportamiento correcto en este caso sería que s1 mantenga el bloqueo exclusivo hasta después de su confirmación. –

+0

@Quassnoi: ¿Qué lee otra transacción después de la actualización y antes de la confirmación? Si tiene, por ejemplo, READ_COMMITED, dejar que la otra transacción vea el valor '1' sería incorrecto (se vería en la otra transacción). '0' es el único otro valor lógico que la otra transacción simultánea debería poder ver. Entonces, o bien esta condición de carrera debería poder ocurrir (y posiblemente dar como resultado una falla), o la base de datos necesita serializar de alguna manera las transacciones. –

Cuestiones relacionadas