sql >> Base de Datos >  >> RDS >> Database

El problema de actualización perdida en transacciones concurrentes

El problema de actualización perdida ocurre cuando 2 transacciones simultáneas intentan leer y actualizar los mismos datos. Entendamos esto con la ayuda de un ejemplo.

Supongamos que tenemos una tabla llamada "Producto" que almacena la identificación, el nombre y los artículos en stock para un producto.

Se utiliza como parte de un sistema en línea que muestra la cantidad de artículos en stock para un producto en particular y, por lo tanto, debe actualizarse cada vez que se realiza una venta de ese producto.

La tabla se ve así:

Id

Nombre

Artículos en stock

1

Ordenadores portátiles

12

Ahora considere un escenario en el que llega un usuario e inicia el proceso de compra de una computadora portátil. Esto iniciará una transacción. Llamemos a esta transacción transacción 1.

Al mismo tiempo, otro usuario inicia sesión en el sistema e inicia una transacción, llamemos a esta transacción 2. Observe la siguiente figura.

La transacción 1 lee los artículos en stock para portátiles, que son 12. Un poco más tarde, la transacción 2 lee el valor de ItemsinStock para portátiles, que seguirá siendo 12 en este momento. Luego, la transacción 2 vende tres computadoras portátiles, poco antes de que la transacción 1 venda 2 artículos.

La transacción 2 luego completará su ejecución primero y actualizará ItemsinStock a 9 ya que vendió tres de las 12 computadoras portátiles. La transacción 1 se compromete. Dado que la transacción 1 vendió dos artículos, actualiza ItemsinStock a 10.

Esto es incorrecto, la cifra correcta es 12-3-2 =7

Ejemplo práctico de problema de actualización perdida

Echemos un vistazo al problema de actualización perdida en acción en SQL Server. Como siempre, primero, crearemos una tabla y le agregaremos algunos datos ficticios.

Como siempre, asegúrese de tener una copia de seguridad adecuada antes de jugar con el nuevo código. Si no está seguro, consulte este artículo sobre la copia de seguridad de SQL Server.

Ejecute el siguiente script en su servidor de base de datos.

<span style="font-size: 14px;">CREATE DATABASE pos;

USE pos;

CREATE TABLE products
(
	Id INT PRIMARY KEY,
	Name VARCHAR(50) NOT NULL,
	ItemsinStock INT NOT NULL

)

INSERT into products

VALUES 
(1, 'Laptop', 12),
(2, 'Iphon', 15),
(3, 'Tablets', 10)</span>

Ahora, abra dos instancias de SQL Server Management Studio una al lado de la otra. Ejecutaremos una transacción en cada una de estas instancias.

Agregue el siguiente script a la primera instancia de SSMS.

<span style="font-size: 14px;">USE pos;

-- Transaction 1

BEGIN TRAN

DECLARE @ItemsInStock INT

SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1

WaitFor Delay '00:00:12'
SET @ItemsInStock = @ItemsInStock - 2

UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1

Print @ItemsInStock
Commit Transaction</span>

Este es el script para la transacción 1. Aquí comenzamos la transacción y declaramos una variable de tipo entero “@ItemsInStock”. El valor de esta variable se establece en el valor de la columna ItemsinStock para el registro con Id 1 de la tabla de productos. Luego se agrega un retraso de 12 segundos para que la transacción 2 pueda completar su ejecución antes que la transacción 1. Después del retraso, el valor de la variable @ItemsInStock se decrementa en 2, lo que significa la venta de 2 productos.

Finalmente, el valor de la columna ItemsinStock para el registro con Id 1 se actualiza con el valor de la variable @ItemsInStock. Luego imprimimos el valor de la variable @ItemsInStock en la pantalla y confirmamos la transacción.

En la segunda instancia de SSMS, agregamos el script para la transacción 2, que es el siguiente:

<span style="font-size: 14px;">USE pos;

-- Transaction 2

BEGIN TRAN

DECLARE @ItemsInStock INT

SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1

WaitFor Delay '00:00:3'
SET @ItemsInStock = @ItemsInStock - 3

UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1

Print @ItemsInStock
Commit Transaction</span>

El script para la transacción 2 es similar a la transacción 1. Sin embargo, aquí en la transacción 2, la demora es solo de tres segundos y la disminución en el valor de la variable @ItemsInStock es tres, ya que es una venta de tres artículos.

Ahora, ejecute la transacción 1 y luego la transacción 2. Verá que la transacción 2 completa su ejecución primero. Y el valor impreso para la variable @ItemsInStock será 9. Después de un tiempo, la transacción 1 también completará su ejecución y el valor impreso para su variable @ItemsInStock será 10.

Ambos valores son incorrectos, el valor real de la columna ItemsInStock para el producto con Id. 1 debe ser 7.

NOTA:

Es importante tener en cuenta aquí que el problema de actualización perdida solo ocurre con niveles de aislamiento de transacciones de lectura confirmada y lectura no confirmada. Con todos los demás niveles de aislamiento de transacciones, este problema no ocurre.

Leer nivel de aislamiento de transacciones repetibles

Actualicemos el nivel de aislamiento para que ambas transacciones se lean repetibles y veamos si ocurre el problema de actualización perdida. Pero antes de eso, ejecute la siguiente instrucción para actualizar el valor de ItemsInStock nuevamente a 12.

Update products SET ItemsinStock = 12

Script para la transacción 1

<span style="font-size: 14px;">USE pos;

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
-- Transaction 1

BEGIN TRAN
DECLARE @ItemsInStock INT

SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1

WaitFor Delay '00:00:12'
SET @ItemsInStock = @ItemsInStock - 2

UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1

Print @ItemsInStock
Commit Transaction</span>

Script para la transacción 2

<span style="font-size: 14px;">USE pos;

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
-- Transaction 2

BEGIN TRAN
DECLARE @ItemsInStock INT

SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1

WaitFor Delay '00:00:3'
SET @ItemsInStock = @ItemsInStock - 3

UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1

Print @ItemsInStock
Commit Transaction</span>

Aquí, en ambas transacciones, hemos establecido el nivel de aislamiento en lectura repetible.

Ahora ejecute la transacción 1 y luego ejecute inmediatamente la transacción 2. A diferencia del caso anterior, la transacción 2 tendrá que esperar a que la transacción 1 se comprometa. Después de eso, ocurre el siguiente error para la transacción 2:

Mensaje 1205, Nivel 13, Estado 51, Línea 15

La transacción (ID de proceso 55) se interbloqueó en los recursos de bloqueo con otro proceso y se eligió como víctima del interbloqueo. Vuelva a ejecutar la transacción.

Este error ocurre porque la lectura repetible bloquea el recurso que la transacción 1 está leyendo o actualizando y crea un interbloqueo en la otra transacción que intenta acceder al mismo recurso.

El error dice que la transacción 2 tiene un interbloqueo en un recurso con otro proceso y que esta transacción ha sido bloqueada por el interbloqueo. Esto significa que a la otra transacción se le dio acceso al recurso mientras que esta transacción estaba bloqueada y no se le dio acceso al recurso.

También dice que vuelva a ejecutar la transacción ya que el recurso está libre ahora. Ahora, si vuelve a ejecutar la transacción 2, verá el valor correcto de los artículos en stock, es decir, 7. Esto se debe a que la transacción 1 ya había disminuido el valor de IteminStock en 2, la transacción 2 lo reduce aún más en 3, por lo tanto, 12 – (2+ 3) =7.