Problemet med förlorad uppdatering uppstår när 2 samtidiga transaktioner försöker läsa och uppdatera samma data. Låt oss förstå detta med hjälp av ett exempel.
Anta att vi har en tabell som heter "Produkt" som lagrar id, namn och artiklar i lager för en produkt.
Den används som en del av ett onlinesystem som visar antalet artiklar i lager för en viss produkt och måste därför uppdateras varje gång en försäljning av den produkten görs.
Tabellen ser ut så här:
Id | Namn | Artiklar i lager |
1 | Bärbara datorer | 12 |
Tänk nu på ett scenario där en användare kommer och påbörjar processen att köpa en bärbar dator. Detta kommer att initiera en transaktion. Låt oss kalla denna transaktion, transaktion 1.
Samtidigt som en annan användare loggar in i systemet och initierar en transaktion, låt oss kalla denna transaktion 2. Ta en titt på följande bild.
Transaktion 1 läser artiklarna i lager för bärbara datorer som är 12. Lite senare läser transaktion 2 värdet för Artiklar i Lager för bärbara datorer som fortfarande kommer att vara 12 vid denna tidpunkt. Transaktion 2 säljer sedan tre bärbara datorer, kort innan transaktion 1 säljer 2 artiklar.
Transaktion 2 kommer då att slutföra sin exekvering först och uppdatera ItemsinStock till 9 eftersom den sålde tre av de 12 bärbara datorerna. Transaktion 1 förbinder sig. Eftersom transaktion 1 sålde två artiklar uppdateras ItemsinStock till 10.
Detta är felaktigt, den korrekta siffran är 12-3-2 =7
Fungerande exempel på problem med förlorad uppdatering
Låt oss ta en titt på problemet med förlorad uppdatering i aktion i SQL Server. Som alltid kommer vi först att skapa en tabell och lägga till lite dummydata i den.
Som alltid, se till att du är ordentligt säkerhetskopierad innan du spelar med ny kod. Om du inte är säker, läs den här artikeln om SQL Server-säkerhetskopiering.
Kör följande skript på din databasserver.
<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>
Öppna nu två SQL Server Management Studio-instanser sida vid sida. Vi kommer att köra en transaktion i vart och ett av dessa fall.
Lägg till följande skript till den första instansen av 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>
Detta är skriptet för transaktion 1. Här börjar vi transaktionen och deklarerar en heltalsvariabel "@ItemsInStock". Värdet på denna variabel är inställt på värdet i kolumnen ItemsinStock för posten med Id 1 från produkttabellen. Sedan läggs en fördröjning på 12 sekunder till så att transaktion 2 kan slutföra sin exekvering före transaktion 1. Efter fördröjningen minskas värdet på @ItemsInStock-variabeln med 2 som anger försäljningen av 2 produkter.
Slutligen uppdateras värdet för kolumnen ItemsinStock för posten med Id 1 med värdet för variabeln @ItemsInStock. Vi skriver sedan ut värdet av variabeln @ItemsInStock på skärmen och genomför transaktionen.
I den andra instansen av SSMS lägger vi till skriptet för transaktion 2 som är följande:
<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>
Skriptet för transaktion 2 liknar transaktion 1. Men här i transaktion 2 är fördröjningen endast tre sekunder och minskningen av värdet för variabeln @ItemsInStock är tre, eftersom det är en försäljning av tre föremål.
Kör nu transaktion 1 och sedan transaktion 2. Du kommer att se transaktion 2 slutföra sin exekvering först. Och värdet som skrivs ut för @ItemsInStock-variabeln kommer att vara 9. Efter en tid kommer transaktion 1 också att slutföras och värdet som skrivs ut för dess @ItemsInStock-variabel kommer att vara 10.
Båda dessa värden är felaktiga, det faktiska värdet för kolumnen ItemsInStock för produkten med Id 1 bör vara 7.
OBS:
Det är viktigt att notera här att det förlorade uppdateringsproblemet endast uppstår med läs committed och read uncommitted transaktionsisoleringsnivåer. Med alla andra transaktionsisoleringsnivåer uppstår inte detta problem.
Läs isoleringsnivå för repeterbar transaktion
Låt oss uppdatera isoleringsnivån för att båda transaktionerna ska kunna läsas repeterbara och se om det förlorade uppdateringsproblemet uppstår. Men innan dess, kör följande sats för att uppdatera värdet för ItemsInStock tillbaka till 12.
Update products SET ItemsinStock = 12
Skript för transaktion 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>
Skript för transaktion 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>
Här i båda transaktionerna har vi satt isoleringsnivån till repeterbar läsning.
Kör nu transaktion 1 och kör sedan omedelbart transaktion 2. Till skillnad från det tidigare fallet måste transaktion 2 vänta på att transaktion 1 ska begås. Därefter uppstår följande fel för transaktion 2:
Msg 1205, Level 13, State 51, Line 15
Transaktionen (Process ID 55) låstes på låsresurser med en annan process och har valts som dödlägesoffer. Kör transaktionen igen.
Det här felet uppstår eftersom repeterbar läsning låser resursen som läses eller uppdateras av transaktion 1 och det skapar ett dödläge för den andra transaktionen som försöker komma åt samma resurs.
Felet säger att transaktion 2 har ett dödläge på en resurs med en annan process och att denna transaktion har blockerats av dödläget. Det betyder att den andra transaktionen fick tillgång till resursen medan den här transaktionen blockerades och inte fick tillgång till resursen.
Det står också att köra transaktionen igen eftersom resursen är ledig nu. Nu, om du kör transaktion 2 igen, kommer du att se det korrekta värdet av artiklar i lager, dvs. 7. Detta beror på att transaktion 1 redan hade minskat värdet för IteminStock med 2, transaktion 2 minskar detta ytterligare med 3, därför 12 – (2+) 3) =7.