Database
 sql >> Database >  >> RDS >> Database

Il problema dell'aggiornamento perso nelle transazioni simultanee

Il problema di aggiornamento perso si verifica quando 2 transazioni simultanee tentano di leggere e aggiornare gli stessi dati. Capiamolo con l'aiuto di un esempio.

Supponiamo di avere una tabella denominata "Prodotto" che memorizza id, nome e ItemsinStock per un prodotto.

Viene utilizzato come parte di un sistema online che mostra il numero di articoli in stock per un particolare prodotto e quindi deve essere aggiornato ogni volta che viene effettuata una vendita di quel prodotto.

La tabella si presenta così:

ID

Nome

ItemsinStock

1

Laptop

12

Consideriamo ora uno scenario in cui un utente arriva e avvia il processo di acquisto di un laptop. Questo avvierà una transazione. Chiamiamo questa transazione, transazione 1.

Allo stesso tempo un altro utente accede al sistema e avvia una transazione, chiamiamo questa transazione 2. Dai un'occhiata alla figura seguente.

La transazione 1 legge gli articoli in stock per laptop che è 12. Un po' più tardi la transazione 2 legge il valore per ItemsinStock per laptop che sarà ancora 12 in questo momento. La transazione 2 vende quindi tre laptop, poco prima che la transazione 1 venda 2 articoli.

La transazione 2 completerà quindi prima la sua esecuzione e aggiornerà ItemsinStock a 9 poiché ha venduto tre dei 12 laptop. La transazione 1 si impegna. Poiché la transazione 1 ha venduto due articoli, aggiorna ItemsinStock a 10.

Questo non è corretto, la cifra corretta è 12-3-2 =7

Esempio funzionante di problema di aggiornamento perso

Diamo un'occhiata al problema di aggiornamento perso in azione in SQL Server. Come sempre, per prima cosa creeremo una tabella e vi aggiungeremo alcuni dati fittizi.

Come sempre, assicurati di aver eseguito correttamente il backup prima di giocare con il nuovo codice. Se non sei sicuro, consulta questo articolo sul backup di SQL Server.

Esegui il seguente script sul tuo server di database.

<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>

Ora apri due istanze di SQL Server Management Studio affiancate. Eseguiremo una transazione in ciascuna di queste istanze.

Aggiungi il seguente script alla prima istanza di 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>

Questo è lo script per la transazione 1. Qui iniziamo la transazione e dichiariamo una variabile di tipo intero “@ItemsInStock”. Il valore di questa variabile è impostato sul valore della colonna ItemsinStock per il record con Id 1 dalla tabella prodotti. Quindi viene aggiunto un ritardo di 12 secondi in modo che la transazione 2 possa completare la sua esecuzione prima della transazione 1. Dopo il ritardo, il valore della variabile @ItemsInStock viene decrementato di 2 a significare la vendita di 2 prodotti.

Infine, il valore della colonna ItemsinStock per il record con Id 1 viene aggiornato con il valore della variabile @ItemsInStock. Quindi stampiamo il valore della variabile @ItemsInStock sullo schermo e commettiamo la transazione.

Nella seconda istanza di SSMS, aggiungiamo lo script per la transazione 2 che è il seguente:

<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>

Lo script per la transazione 2 è simile alla transazione 1. Tuttavia, qui nella transazione 2, il ritardo è di soli tre secondi e il decremento del valore per la variabile @ItemsInStock è tre, poiché si tratta di una vendita di tre articoli.

Ora, esegui la transazione 1 e poi la transazione 2. Vedrai che la transazione 2 completa prima la sua esecuzione. E il valore stampato per la variabile @ItemsInStock sarà 9. Dopo qualche tempo anche la transazione 1 completerà la sua esecuzione e il valore stampato per la sua variabile @ItemsInStock sarà 10.

Entrambi questi valori sono errati, il valore effettivo per la colonna ItemsInStock per il prodotto con ID 1 dovrebbe essere 7.

NOTA:

È importante notare qui che il problema dell'aggiornamento perso si verifica solo con i livelli di isolamento delle transazioni di lettura con commit e lettura senza commit. Con tutti gli altri livelli di isolamento delle transazioni, questo problema non si verifica.

Leggi il livello di isolamento della transazione ripetibile

Aggiorniamo il livello di isolamento per entrambe le transazioni da leggere ripetibili e vediamo se si verifica il problema di aggiornamento perso. Ma prima, esegui la seguente istruzione per aggiornare il valore di ItemsInStock a 12.

Update products SET ItemsinStock = 12

Script per la transazione 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 per la transazione 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>

Qui in entrambe le transazioni, abbiamo impostato il livello di isolamento su una lettura ripetibile.

Ora esegui la transazione 1 e quindi esegui immediatamente la transazione 2. A differenza del caso precedente, la transazione 2 dovrà attendere che la transazione 1 si impegni. Successivamente si verifica il seguente errore per la transazione 2:

Msg 1205, Livello 13, Stato 51, Linea 15

La transazione (ID processo 55) è stata bloccata sulle risorse di blocco con un altro processo ed è stata scelta come vittima del deadlock. Riesegui la transazione.

Questo errore si verifica perché la lettura ripetibile blocca la risorsa che viene letta o aggiornata dalla transazione 1 e crea un deadlock sull'altra transazione che tenta di accedere alla stessa risorsa.

L'errore dice che la transazione 2 ha un deadlock su una risorsa con un altro processo e che questa transazione è stata bloccata dal deadlock. Ciò significa che all'altra transazione è stato concesso l'accesso alla risorsa mentre questa transazione è stata bloccata e non è stato concesso l'accesso alla risorsa.

Dice anche di eseguire nuovamente la transazione poiché la risorsa è ora gratuita. Ora, se esegui nuovamente la transazione 2, vedrai il valore corretto degli articoli in magazzino, ad esempio 7. Questo perché la transazione 1 aveva già decrementato il valore di IteminStock di 2, la transazione 2 lo decrementa ulteriormente di 3, quindi 12 – (2+ 3) =7.