Mat y Erwin tienen razón, y solo estoy agregando otra respuesta para ampliar aún más lo que dijeron de una manera que no cabe en un comentario. Dado que sus respuestas no parecen satisfacer a todos, y hubo una sugerencia de que se debería consultar a los desarrolladores de PostgreSQL, y yo soy uno, elaboraré.
El punto importante aquí es que, según el estándar SQL, dentro de una transacción que se ejecuta en el READ COMMITTED
nivel de aislamiento de la transacción, la restricción es que el trabajo de las transacciones no confirmadas no debe ser visible. Cuando el trabajo de las transacciones confirmadas se hace visible, depende de la implementación. Lo que está señalando es una diferencia en cómo dos productos han elegido implementar eso. Ninguna implementación viola los requisitos de la norma.
Esto es lo que sucede dentro de PostgreSQL, en detalle:
S1-1 se ejecuta (1 fila eliminada)
La fila anterior se deja en su lugar, porque S1 aún puede retroceder, pero S1 ahora mantiene un bloqueo en la fila para que cualquier otra sesión que intente modificar la fila espere para ver si S1 se compromete o retrocede. Cualquier lectura de la tabla aún puede ver la fila anterior, a menos que intenten bloquearla con SELECT FOR UPDATE
o SELECT FOR SHARE
.
S2-1 se ejecuta (pero está bloqueado ya que S1 tiene un bloqueo de escritura)
S2 ahora tiene que esperar para ver el resultado de S1. Si S1 retrocediera en lugar de confirmar, S2 eliminaría la fila. Tenga en cuenta que si S1 inserta una nueva versión antes de deshacer, la nueva versión nunca habría estado allí desde la perspectiva de ninguna otra transacción, ni la versión anterior se habría eliminado desde la perspectiva de cualquier otra transacción.
S1-2 carreras (1 fila insertada)
Esta fila es independiente de la anterior. Si hubiera habido una actualización de la fila con id = 1, las versiones antigua y nueva estarían relacionadas, y S2 podría eliminar la versión actualizada de la fila cuando se desbloqueara. Que una nueva fila tenga los mismos valores que alguna fila que existía en el pasado no es lo mismo que una versión actualizada de esa fila.
S1-3 se ejecuta, liberando el bloqueo de escritura
Entonces los cambios de S1 son persistentes. Una fila se ha ido. Se ha agregado una fila.
S2-1 se ejecuta, ahora que puede obtener el bloqueo. Pero informa 0 filas eliminadas. HUH ???
Lo que sucede internamente es que hay un puntero de una versión de una fila a la siguiente versión de esa misma fila si se actualiza. Si se elimina la fila, no hay una versión siguiente. Cuando una READ COMMITTED
transacción se despierta de un bloque en un conflicto de escritura, sigue esa cadena de actualización hasta el final; si la fila no se ha eliminado y si aún cumple con los criterios de selección de la consulta, se procesará. Esta fila se ha eliminado, por lo que la consulta de S2 continúa.
S2 puede o no llegar a la nueva fila durante su exploración de la tabla. Si lo hace, verá que la nueva fila se creó después de DELETE
que se inició la instrucción de S2 , por lo que no es parte del conjunto de filas visibles para ella.
Si PostgreSQL reiniciara la declaración DELETE completa de S2 desde el principio con una nueva instantánea, se comportaría igual que SQL Server. La comunidad PostgreSQL no ha elegido hacer eso por razones de rendimiento. En este caso simple, nunca notaría la diferencia en el rendimiento, pero si tuviera diez millones de filas en un DELETE
momento en que se bloqueó, ciertamente lo haría. Aquí hay una compensación donde PostgreSQL ha elegido el rendimiento, ya que la versión más rápida aún cumple con los requisitos del estándar.
S2-2 se ejecuta, informa una violación de restricción de clave única
Por supuesto, la fila ya existe. Esta es la parte menos sorprendente de la imagen.
Si bien aquí hay un comportamiento sorprendente, todo está en conformidad con el estándar SQL y dentro de los límites de lo que es "específico de implementación" de acuerdo con el estándar. Ciertamente puede ser sorprendente si está asumiendo que el comportamiento de alguna otra implementación estará presente en todas las implementaciones, pero PostgreSQL se esfuerza mucho por evitar fallas de serialización en el READ COMMITTED
nivel de aislamiento, y permite algunos comportamientos que difieren de otros productos para lograrlo.
Ahora, personalmente, no soy un gran admirador del READ COMMITTED
nivel de aislamiento de transacciones en la implementación de ningún producto. Todos permiten que las condiciones de carrera creen comportamientos sorprendentes desde un punto de vista transaccional. Una vez que alguien se acostumbra a los comportamientos extraños permitidos por un producto, tiende a considerar que eso es "normal" y que las compensaciones elegidas por otro producto son extrañas. Pero cada producto tiene que hacer algún tipo de compensación por cualquier modo que no se implemente realmente SERIALIZABLE
. Donde los desarrolladores de PostgreSQL han elegido trazar la línea READ COMMITTED
es minimizar el bloqueo (las lecturas no bloquean las escrituras y las escrituras no bloquean las lecturas) y minimizar la posibilidad de fallas de serialización.
El estándar requiere que las SERIALIZABLE
transacciones sean las predeterminadas, pero la mayoría de los productos no lo hacen porque causa un impacto en el rendimiento en los niveles de aislamiento de transacciones más laxos. Algunos productos ni siquiera proporcionan transacciones verdaderamente serializables cuando SERIALIZABLE
se elige, especialmente Oracle y versiones de PostgreSQL anteriores a 9.1. Pero el uso de SERIALIZABLE
transacciones reales es la única forma de evitar efectos sorprendentes de las condiciones de carrera, y las SERIALIZABLE
transacciones siempre deben bloquearse para evitar las condiciones de carrera o revertir algunas transacciones para evitar una condición de carrera en desarrollo. La implementación más común de las SERIALIZABLE
transacciones es el bloqueo estricto de dos fases (S2PL), que tiene fallas tanto de bloqueo como de serialización (en forma de puntos muertos).
Revelación completa: trabajé con Dan Ports de MIT para agregar transacciones verdaderamente serializables a PostgreSQL versión 9.1 utilizando una nueva técnica llamada Aislamiento de instantáneas serializables.
READ COMMITTED
transacciones, tiene una condición de carrera: ¿qué sucedería si otra transacción insertara una nueva fila después del primerDELETE
inicio y antes de queDELETE
comenzara el segundo ? Con transacciones menos estrictas queSERIALIZABLE
las dos formas principales de cerrar las condiciones de carrera es a través de la promoción de un conflicto (pero eso no ayuda cuando se elimina la fila) y la materialización de un conflicto. Puede materializar el conflicto al tener una tabla "id" que se actualizó para cada fila eliminada, o al bloquear explícitamente la tabla. O use reintentos por error.Creo que esto es por diseño, de acuerdo con la descripción del nivel de aislamiento de lectura confirmada para PostgreSQL 9.2:
La fila se inserta en la
S1
que no existía aún cuandoS2
que estáDELETE
comenzaron. Por lo tanto, no se verá por la eliminación en el puntoS2
( 1 ) anterior. El queS1
borró es ignorado porS2
's deDELETE
acuerdo con ( 2 ).Entonces
S2
, la eliminación no hace nada. Sin embargo, cuando aparece el inserto, ese sí veS1
el inserto:Entonces, el intento de inserción por
S2
falla con la violación de restricción.Continuando leyendo ese documento, usando lectura repetible o incluso serializable no resolvería su problema por completo: la segunda sesión fallaría con un error de serialización en la eliminación.
Sin embargo, esto le permitirá volver a intentar la transacción.
fuente
Estoy completamente de acuerdo con la excelente respuesta de @ Mat . Solo escribo otra respuesta, porque no cabe en un comentario.
En respuesta a su comentario: El
DELETE
in S2 ya está conectado a una versión de fila particular. Dado que esto es asesinado por S1 mientras tanto, S2 se considera exitoso. Aunque no es obvio a simple vista, la serie de eventos es prácticamente así:Todo es por diseño. Realmente necesita usar las
SERIALIZABLE
transacciones para sus requisitos y asegurarse de volver a intentar el error de serialización.fuente
Use una clave primaria DEFERRABLE e intente nuevamente.
fuente
También nos enfrentamos a este problema. Nuestra solución es agregar
select ... for update
antesdelete from ... where
. El nivel de aislamiento debe ser Lectura comprometida.fuente